From e1cdbfccde1c9c32d793b80efa036961208d1327 Mon Sep 17 00:00:00 2001
From: Tank Tang <kat@microsoft.com>
Date: Sun, 1 Mar 2020 23:39:43 +0800
Subject: [PATCH] Added the test cases from Azure Core

---
 test/fixtures/files/test.png                | Bin 0 -> 4054 bytes
 test/fixtures/http_invalid_header.xml       |   7 +
 test/support/fixtures.rb                    |  47 ++++-
 test/unit/core/auth/shared_key_lite_test.rb |  51 +++++
 test/unit/core/auth/shared_key_test.rb      |  59 ++++++
 test/unit/core/auth/signer_test.rb          |  30 +++
 test/unit/core/filter_service_test.rb       |  41 ++++
 test/unit/core/http/http_error_test.rb      | 113 +++++++++++
 test/unit/core/http/http_request_test.rb    | 201 ++++++++++++++++++++
 test/unit/core/http/http_response_test.rb   |  20 ++
 test/unit/core/service_test.rb              |  73 +++++++
 test/unit/core/utility_test.rb              | 123 ++++++++++++
 12 files changed, 764 insertions(+), 1 deletion(-)
 create mode 100644 test/fixtures/files/test.png
 create mode 100644 test/fixtures/http_invalid_header.xml
 create mode 100644 test/unit/core/auth/shared_key_lite_test.rb
 create mode 100644 test/unit/core/auth/shared_key_test.rb
 create mode 100644 test/unit/core/auth/signer_test.rb
 create mode 100644 test/unit/core/filter_service_test.rb
 create mode 100644 test/unit/core/http/http_error_test.rb
 create mode 100644 test/unit/core/http/http_request_test.rb
 create mode 100644 test/unit/core/http/http_response_test.rb
 create mode 100644 test/unit/core/service_test.rb
 create mode 100644 test/unit/core/utility_test.rb

diff --git a/test/fixtures/files/test.png b/test/fixtures/files/test.png
new file mode 100644
index 0000000000000000000000000000000000000000..afbbb4f8b77c0826e00104e7a685a732184bb5f5
GIT binary patch
literal 4054
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

literal 0
HcmV?d00001

diff --git a/test/fixtures/http_invalid_header.xml b/test/fixtures/http_invalid_header.xml
new file mode 100644
index 0000000..6ddfedc
--- /dev/null
+++ b/test/fixtures/http_invalid_header.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<Error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
+  <Code>InvalidHeaderValue</Code>
+  <Message>The value for one of the HTTP headers is not in the correct format.\nRequestId:266be2bc-0001-0018-1256-d3bb92000000\nTime:2016-07-01T05:06:32.5922690Z</Message>
+  <HeaderName>Range</HeaderName>
+  <HeaderValue>bytes=0-512</HeaderValue>
+</Error>
diff --git a/test/support/fixtures.rb b/test/support/fixtures.rb
index 087df4f..4398aa1 100644
--- a/test/support/fixtures.rb
+++ b/test/support/fixtures.rb
@@ -21,7 +21,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 # THE SOFTWARE.
 #--------------------------------------------------------------------------
-require "pathname"
+require "azure/core/http/retry_policy"
 
 Fixtures = Hash.new do |hash, fixture|
   if path = Fixtures.xml?(fixture)
@@ -49,3 +49,48 @@ end
 def Fixtures.json?(fixture)
   file?("#{fixture}.json")
 end
+
+module Azure
+  module Core
+    Fixtures = Hash.new do |hash, fixture|
+      if path = Fixtures.xml?(fixture)
+        hash[fixture] = path.read
+      elsif path = Fixtures.file?(fixture)
+        hash[fixture] = path
+      end
+    end
+    def Fixtures.root
+      Pathname("../../fixtures").expand_path(__FILE__)
+    end
+    def Fixtures.file?(fixture)
+      path = root.join(fixture)
+      path.file? && path
+    end
+    def Fixtures.xml?(fixture)
+      file?("#{fixture}.xml")
+    end
+    
+    class FixtureRetryPolicy < Azure::Core::Http::RetryPolicy
+      def initialize
+        super &:should_retry?
+      end
+      def should_retry?(response, retry_data)
+        retry_data[:error].inspect.include?('Error: Retry')
+      end
+    end
+
+    class NewUriRetryPolicy < Azure::Core::Http::RetryPolicy
+      def initialize
+        @count = 1
+        super &:should_retry?
+      end
+
+      def should_retry?(response, retry_data)
+        retry_data[:uri] = URI.parse "http://bar.com"
+        @count = @count - 1
+        @count >= 0
+      end
+    end
+
+  end
+end
diff --git a/test/unit/core/auth/shared_key_lite_test.rb b/test/unit/core/auth/shared_key_lite_test.rb
new file mode 100644
index 0000000..75a49e9
--- /dev/null
+++ b/test/unit/core/auth/shared_key_lite_test.rb
@@ -0,0 +1,51 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# 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 'test_helper'
+require 'azure/core/auth/shared_key_lite'
+
+describe Azure::Core::Auth::SharedKeyLite do
+  subject { Azure::Core::Auth::SharedKeyLite.new 'account-name', 'YWNjZXNzLWtleQ==' }
+
+  let(:verb) { 'POST' }
+  let(:uri) { URI.parse 'http://dummy.uri/resource' }
+  let(:headers) do
+    {
+      'Content-MD5' => 'foo',
+      'Content-Type' => 'foo',
+      'Date' => 'foo'
+    }
+  end
+  let(:headers_without_date) {
+    headers_without_date = headers.clone
+    headers_without_date.delete 'Date'
+    headers_without_date
+  }
+
+  describe 'sign' do
+    it 'creates a signature from the provided HTTP method, uri, and reduced set of standard headers' do
+      subject.sign(verb, uri, headers).must_equal 'account-name:vVFnj/+27JFABZgpt5H8g/JVU2HuWFnjv5aeUIxQvBE='
+    end
+
+    it 'ignores standard headers other than Content-MD5, Content-Type, and Date' do
+      subject.sign(verb, uri, headers.merge({'Content-Encoding' => 'foo'})).must_equal 'account-name:vVFnj/+27JFABZgpt5H8g/JVU2HuWFnjv5aeUIxQvBE='
+    end
+
+    it 'throws IndexError when there is no Date header' do
+      assert_raises IndexError do
+        subject.sign(verb, uri, headers_without_date)
+      end
+    end
+  end
+end
diff --git a/test/unit/core/auth/shared_key_test.rb b/test/unit/core/auth/shared_key_test.rb
new file mode 100644
index 0000000..0cd8646
--- /dev/null
+++ b/test/unit/core/auth/shared_key_test.rb
@@ -0,0 +1,59 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# 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 'test_helper'
+require 'azure/core/auth/shared_key'
+
+describe Azure::Core::Auth::SharedKey do
+  subject { Azure::Core::Auth::SharedKey.new 'account-name', 'YWNjZXNzLWtleQ==' }
+
+  let(:verb) { 'POST' }
+  let(:uri) { URI.parse 'http://dummy.uri/resource' }
+  let(:headers) do
+    {
+      'Content-Encoding' => 'foo',
+      'Content-Language' => 'foo',
+      'Content-Length' => 'foo',
+      'Content-MD5' => 'foo',
+      'Content-Type' => 'foo',
+      'Date' => 'foo',
+      'If-Modified-Since' => 'foo',
+      'If-Match' => 'foo',
+      'If-None-Match' => 'foo',
+      'If-Unmodified-Since' => 'foo',
+      'Range' => 'foo',
+      'x-ms-ImATeapot' => 'teapot',
+      'x-ms-ShortAndStout' => 'True',
+      'x-ms-reserve-spaces' => 'two  speces'
+    }
+  end
+
+  describe 'sign' do
+    it 'creates a signature from the provided HTTP method, uri, and a specific set of standard headers' do
+      subject.sign(verb, uri, headers).must_equal 'account-name:TVilUAfUwtHIVp+eonglFDXfS5r0/OE0/vVX3GHcaxU='
+    end
+  end
+
+  describe 'canonicalized_headers' do
+    it 'creates a canonicalized header string' do
+      subject.canonicalized_headers(headers).must_equal "x-ms-imateapot:teapot\nx-ms-reserve-spaces:two  speces\nx-ms-shortandstout:True"
+    end
+  end
+
+  describe 'canonicalized_resource' do
+    it 'creates a canonicalized resource string' do
+      subject.canonicalized_resource(uri).must_equal '/account-name/resource'
+    end
+  end
+end
diff --git a/test/unit/core/auth/signer_test.rb b/test/unit/core/auth/signer_test.rb
new file mode 100644
index 0000000..917194b
--- /dev/null
+++ b/test/unit/core/auth/signer_test.rb
@@ -0,0 +1,30 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# 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 "test_helper"
+require "azure/core/auth/signer"
+
+describe Azure::Core::Auth::Signer do
+  subject { Azure::Core::Auth::Signer.new "YWNjZXNzLWtleQ==" }
+
+  it "decodes the base64 encoded access_key" do
+    subject.access_key.must_equal "access-key"
+  end
+
+  describe "sign" do
+    it "creates a signature for the body, as a base64 encoded string, which represents a HMAC hash using the access_key" do
+      subject.sign("body").must_equal "iuUxVhs1E7PeSNx/90ViyJNO24160qYpoWeCcOsnMoM="
+    end
+  end
+end
diff --git a/test/unit/core/filter_service_test.rb b/test/unit/core/filter_service_test.rb
new file mode 100644
index 0000000..d16b222
--- /dev/null
+++ b/test/unit/core/filter_service_test.rb
@@ -0,0 +1,41 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# 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 'test_helper'
+require 'azure/core'
+require "azure/core/http/debug_filter"
+require "azure/core/http/retry_policy"
+
+describe 'Azure core service' do
+  subject do
+    Azure::Core::FilteredService.new
+  end
+
+  it 'works with default' do
+    subject.filters.count.must_equal 0
+  end
+
+  it 'works with a debug filter' do
+    service = Azure::Core::FilteredService.new
+    service.with_filter Azure::Core::Http::DebugFilter.new
+    service.filters.count.must_equal 1
+  end
+
+  it 'works with retry policy filter' do
+    service = Azure::Core::FilteredService.new
+    service.with_filter Azure::Core::Http::DebugFilter.new
+    service.with_filter Azure::Core::Http::RetryPolicy.new
+    service.filters.count.must_equal 2
+  end
+end
diff --git a/test/unit/core/http/http_error_test.rb b/test/unit/core/http/http_error_test.rb
new file mode 100644
index 0000000..49b5040
--- /dev/null
+++ b/test/unit/core/http/http_error_test.rb
@@ -0,0 +1,113 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# 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 'test_helper'
+require 'azure/core/http/http_error'
+
+describe Azure::Core::Http::HTTPError do
+  let :http_response do
+    stub(body: Azure::Core::Fixtures[:http_error], status_code: 409, uri: 'http://dummy.uri', headers: { 'Content-Type' => 'application/atom+xml' })
+  end
+
+  subject do
+    Azure::Core::Http::HTTPError.new(http_response)
+  end
+
+  it 'is an instance of Azure::Core::Error' do
+    subject.must_be_kind_of Azure::Core::Error
+  end
+
+  it 'lets us see the original uri' do
+    subject.uri.must_equal 'http://dummy.uri'
+  end
+
+  it "lets us see the errors'status code" do
+    subject.status_code.must_equal 409
+  end
+
+  it "lets us see the error's type" do
+    subject.type.must_equal 'TableAlreadyExists'
+  end
+
+  it "lets us see the error's description" do
+    subject.description.must_equal 'The table specified already exists.'
+  end
+
+  it 'generates an error message that wraps both the type and description' do
+    subject.message.must_equal 'TableAlreadyExists (409): The table specified already exists.'
+  end
+
+  describe 'with invalid http_response body' do
+    let :http_response do
+      stub(:body => "\r\nInvalid request\r\n", :status_code => 409, :uri => 'http://dummy.uri', headers: {})
+    end
+
+    it 'sets the type to unknown if the response body is not an XML' do
+      subject.type.must_equal 'Unknown'
+      subject.description.must_equal 'Invalid request'
+    end
+  end
+
+  describe 'with invalid headers' do
+    let :http_response do
+      stub(body: Azure::Core::Fixtures[:http_invalid_header], status_code: 400, uri: 'http://dummy.uri', headers: { 'Content-Type' => 'application/atom+xml'})
+    end
+
+    it 'sets the invalid header in the error details' do
+      subject.status_code.must_equal 400
+      subject.type.must_equal 'InvalidHeaderValue'
+      subject.description.must_include 'The value for one of the HTTP headers is not in the correct format'
+      subject.header.must_equal 'Range'
+      subject.header_value.must_equal 'bytes=0-512'
+    end
+  end
+
+  describe 'with JSON payload' do
+    let :http_response do
+      body = "{\"odata.error\":{\"code\":\"ErrorCode\",\"message\":{\"lang\":\"en-US\",\"value\":\"ErrorDescription\"}}}"
+      stub(body: body, status_code: 400, uri: 'http://dummy.uri', headers: { 'Content-Type' => 'application/json' })
+    end
+
+    it 'parse error response with JSON payload' do
+      subject.status_code.must_equal 400
+      subject.type.must_equal 'ErrorCode'
+      subject.description.must_include 'ErrorDescription'
+    end
+  end
+
+  describe 'with unknown payload' do
+    let :http_response do
+      body = 'Unknown Payload Format with Unknown Error Description'
+      stub(body: body, status_code: 400, uri: 'http://dummy.uri', headers: {})
+    end
+
+    it 'parse error response with JSON payload' do
+      subject.status_code.must_equal 400
+      subject.type.must_equal 'Unknown'
+      subject.description.must_include 'Error Description'
+    end
+  end
+
+  describe 'with no response body' do
+    let :http_response do
+      body = ''
+      stub(body: body, status_code: 404, uri: 'http://dummy.uri', headers: {}, reason_phrase: 'dummy reason')
+    end
+
+    it 'message has value assigned from reason_phrase' do
+      subject.status_code.must_equal 404
+      subject.message.must_equal 'Unknown (404): dummy reason'
+    end
+  end
+end
diff --git a/test/unit/core/http/http_request_test.rb b/test/unit/core/http/http_request_test.rb
new file mode 100644
index 0000000..fa781fe
--- /dev/null
+++ b/test/unit/core/http/http_request_test.rb
@@ -0,0 +1,201 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# 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 'test_helper'
+require 'azure/core/http/http_request'
+
+describe Azure::Core::Http::HttpRequest do
+  let(:uri) { URI('http://example.com') }
+
+  describe ' default_headers ' do
+    subject do
+      Azure::Core::Http::HttpRequest.new(:get, uri, body: nil, current_time: 'Thu, 04 Oct 2012 06:38:27 GMT')
+    end
+
+    it 'sets the x-ms-date header to the current_time' do
+      subject.headers['x-ms-date'] = 'Thu, 04 Oct 2012 06:38:27 GMT'
+    end
+
+    it 'sets the x-ms-version header to the current API version' do
+      subject.headers['x-ms-version'] = '2011-08-18'
+    end
+
+    it 'sets the DataServiceVersion header to the current API version' do
+      subject.headers['DataServiceVersion'] = '1.0;NetFx'
+    end
+
+    it 'sets the MaxDataServiceVersion header to the current max` API version' do
+      subject.headers['MaxDataServiceVersion'] = '2.0;NetFx'
+    end
+  end
+
+  describe 'when passed custom headers' do
+    subject do
+      Azure::Core::Http::HttpRequest.new(:get,
+                                         uri,
+                                         body: nil,
+                                         headers: {
+                                             'blah' => 'something',
+                                             'x-ms-version' => '123'
+                                         })
+    end
+
+    it 'should have overridden the value of x-ms-version' do
+      subject.headers['x-ms-version'].must_equal '123'
+    end
+
+    it 'should have added in the blah = something header' do
+      subject.headers['blah'].must_equal 'something'
+    end
+
+  end
+
+  describe ' when passed a body ' do
+    describe " of type IO" do
+      subject do
+        file = File.open(File.expand_path("../../../../fixtures/files/test.png", __FILE__))
+        Azure::Core::Http::HttpRequest.new(:post, uri, body: file)
+      end
+
+      it 'sets the default Content-Type header' do
+        subject.headers['Content-Type'].must_equal 'application/atom+xml; charset=utf-8'
+      end
+
+      it 'sets the Content-Length header' do
+        subject.headers['Content-Length'].must_equal '4054'
+      end
+
+      it 'sets the Content-MD5 header to a Base64 encoded representation of the MD5 hash of the body' do
+        subject.headers['Content-MD5'].must_equal 'nxTCAVCgA9fOTeV8KY8Pug=='
+      end
+    end
+
+    describe 'of type Tempfile' do
+      subject do
+        tempfile = Tempfile.open('azure')
+        file = File.open(File.expand_path('../../../../fixtures/files/test.png', __FILE__))
+        IO.copy_stream(file, tempfile)
+
+        Azure::Core::Http::HttpRequest.new(:post, uri, body: tempfile)
+      end
+
+      it 'sets the default Content-Type header' do
+        subject.headers['Content-Type'].must_equal 'application/atom+xml; charset=utf-8'
+      end
+
+      it 'sets the Content-Length header' do
+        subject.headers['Content-Length'].must_equal '4054'
+      end
+
+      it 'sets the Content-MD5 header to a Base64 encoded representation of the MD5 hash of the body' do
+        subject.headers['Content-MD5'].must_equal 'nxTCAVCgA9fOTeV8KY8Pug=='
+      end
+    end
+
+    describe ' of type StringIO' do
+      subject do
+        Azure::Core::Http::HttpRequest.new(:post, uri, body: StringIO.new('<body/>'))
+      end
+
+      it 'sets the default Content-Type header' do
+        subject.headers['Content-Type'].must_equal 'application/atom+xml; charset=utf-8'
+      end
+
+      it 'sets the Content-Length header' do
+        subject.headers['Content-Length'].must_equal '7'
+      end
+
+      it 'sets the Content-MD5 header to a Base64 encoded representation of the MD5 hash of the body' do
+        subject.headers['Content-MD5'].must_equal 'PNeJy7qyzV4XUoBBHkVu0g=='
+      end
+    end
+
+
+    describe ' of type String' do
+      subject do
+        Azure::Core::Http::HttpRequest.new(:post, uri, body: '<body/>')
+      end
+
+      it 'sets the default Content-Type header' do
+        subject.headers['Content-Type'].must_equal 'application/atom+xml; charset=utf-8'
+      end
+
+      it 'sets the Content-Length header' do
+        subject.headers['Content-Length'].must_equal '7'
+      end
+
+      it 'sets the Content-MD5 header to a Base64 encoded representation of the MD5 hash of the body' do
+        subject.headers['Content-MD5'].must_equal 'PNeJy7qyzV4XUoBBHkVu0g=='
+      end
+    end
+  end
+
+  describe ' when the body is nil ' do
+    subject do
+      Azure::Core::Http::HttpRequest.new(:get, uri)
+    end
+
+    it 'leaves the Content-Type, Content-Length, and Content-MD5 headers blank' do
+      subject.headers['Content-Length'].must_equal '0'
+      subject.headers['Content-MD5'].must_be_nil
+    end
+  end
+
+  describe '#call' do
+
+    let(:mock_conn) do
+      conn = mock
+      conn.expects(:run_request, [uri, nil, nil]).returns(mock_res)
+      conn
+    end
+
+    subject do
+      sub = Azure::Core::Http::HttpRequest.new(:get, uri)
+      sub.expects(:http_setup).returns(mock_conn)
+      sub
+    end
+
+    describe 'on success' do
+      let(:body) { '</body>' }
+
+      let(:mock_res) do
+        res = mock
+        res.expects(:success?).returns(true)
+        res.expects(:body).returns(body)
+        res
+      end
+
+      it 'should return a response' do
+        subject.call.body.must_equal(body)
+      end
+    end
+
+    describe 'on failure' do
+      let(:body) { 'OH NO!!' }
+
+      let(:mock_res) do
+        res = mock
+        res.expects(:success?).returns(false).at_least_once
+        res.expects(:status).returns(401).at_least_once
+        res.expects(:body).returns(body).at_least_once
+        res.expects(:headers).returns({}).at_least_once
+        res
+      end
+
+      it 'should return a response' do
+       -> { subject.call }.must_raise(Azure::Core::Http::HTTPError)
+      end
+    end
+  end
+end
diff --git a/test/unit/core/http/http_response_test.rb b/test/unit/core/http/http_response_test.rb
new file mode 100644
index 0000000..95a127d
--- /dev/null
+++ b/test/unit/core/http/http_response_test.rb
@@ -0,0 +1,20 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# 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 'test_helper'
+require 'azure/core/http/http_response'
+
+describe Azure::Core::Http::HttpResponse do
+  # TODO: fill in with better tests.
+end
diff --git a/test/unit/core/service_test.rb b/test/unit/core/service_test.rb
new file mode 100644
index 0000000..2f706c9
--- /dev/null
+++ b/test/unit/core/service_test.rb
@@ -0,0 +1,73 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# 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 'test_helper'
+require 'azure/core'
+
+describe 'Azure core service' do
+  subject do
+    Azure::Core::Service.new
+  end
+
+  it 'generate_uri should return URI instance' do
+    subject.host = 'http://dumyhost.uri'
+    subject.generate_uri.must_be_kind_of ::URI
+    subject.generate_uri.to_s.must_equal 'http://dumyhost.uri/'
+  end
+
+  it 'generate_uri should add path to the url' do
+    subject.generate_uri('resource/entity/').path.must_equal '/resource/entity/'
+  end
+
+  it 'generate_uri should correctly join the path if host url contained a path' do
+    subject.host = 'http://dummy.uri/host/path'
+    subject.generate_uri('resource/entity/').path.must_equal '/host/path/resource/entity/'
+  end
+
+  it 'generate_uri should encode the keys' do
+    subject.generate_uri('', {'key !' => 'value'}).query.must_include 'key+%21=value'
+  end
+
+  it 'generate_uri should encode the values' do
+    subject.generate_uri('', {'key' => 'value !'}).query.must_include 'key=value+%21'
+  end
+
+  it 'generate_uri should set query string to the encoded result' do
+    subject.generate_uri('', {'key' => 'value !', 'key !' => 'value'}).query.must_equal 'key=value+%21&key+%21=value'
+  end
+
+  it 'generate_uri should override the default timeout' do
+    subject.generate_uri('', {'timeout' => 45}).query.must_equal 'timeout=45'
+  end
+
+  it 'generate_uri should not include any query parameters' do
+    subject.generate_uri('', nil).query.must_be_nil
+  end
+
+  it 'generate_uri should not re-encode path with spaces' do
+    subject.host = 'http://dumyhost.uri'
+    encoded_path = 'blob%20name%20with%20spaces'
+    uri = subject.generate_uri(encoded_path, nil)
+    uri.host.must_equal 'dumyhost.uri'
+    uri.path.must_equal '/blob%20name%20with%20spaces'
+  end
+
+  it 'generate_uri should not re-encode path with special characters' do
+    subject.host = 'http://dumyhost.uri'
+    encoded_path = 'host/path/%D1%84%D0%B1%D0%B0%D1%84.jpg'
+    uri = subject.generate_uri(encoded_path, nil)
+    uri.host.must_equal 'dumyhost.uri'
+    uri.path.must_equal '/host/path/%D1%84%D0%B1%D0%B0%D1%84.jpg'
+  end
+end
diff --git a/test/unit/core/utility_test.rb b/test/unit/core/utility_test.rb
new file mode 100644
index 0000000..a8f77a3
--- /dev/null
+++ b/test/unit/core/utility_test.rb
@@ -0,0 +1,123 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# 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 'test_helper'
+require 'azure/core/utility'
+
+describe Azure::Core::Logger do
+  subject { Azure::Core::Logger }
+  let(:msg) { "message" }
+
+  after {
+    subject.initialize_external_logger(nil)
+  }
+
+  describe "Log without external logger" do
+    before {
+      subject.initialize_external_logger(nil)
+    }
+
+    it "#info" do
+      out, err = capture_io { subject.info(msg) }
+      assert_equal("\e[37m\e[1m" + msg + "\e[0m\e[0m\n", out)
+    end
+
+    it "#error_with_exit" do
+      out, err = capture_io do
+        error = assert_raises(RuntimeError) do
+          subject.error_with_exit(msg)
+        end
+        assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m", error.message)
+      end
+      assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m\n", out)
+    end
+
+    it "#warn" do
+      out, err = capture_io do
+        warn = subject.warn(msg)
+        assert_equal(msg, warn)
+      end
+      assert_equal("\e[33m" + msg + "\e[0m\n", out)
+    end
+
+    it "#error" do
+      out, err = capture_io do
+        error = subject.error(msg)
+        assert_equal(msg, error)
+      end
+      assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m\n", out)
+    end
+
+    it "#exception_message" do
+      out, err = capture_io do
+        exception = assert_raises(RuntimeError) do
+          subject.exception_message(msg)
+        end
+        assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m", exception.message)
+      end
+      assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m\n", out)
+    end
+
+    it "#success" do
+      out, err = capture_io { subject.success(msg) }
+      assert_equal("\e[32m" + msg + "\n\e[0m", out)
+    end
+  end
+
+  describe "Log with external logger" do
+    let(:fake_output) { StringIO.new }
+
+    before {
+      subject.initialize_external_logger(Logger.new(fake_output))
+    }
+
+    it "#info" do
+      subject.info(msg)
+      assert_match(/INFO -- : #{msg}\n/, fake_output.string)
+    end
+
+    it "#error_with_exit" do
+      error = assert_raises(RuntimeError) do
+        subject.error_with_exit(msg)
+      end
+      assert_match(/ERROR -- : #{msg}\n/, fake_output.string)
+      assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m", error.message)
+    end
+
+    it "#warn" do
+      warn = subject.warn(msg)
+      assert_match(/WARN -- : #{msg}\n/, fake_output.string)
+      assert_equal(msg, warn)
+    end
+
+    it "#error" do
+      error = subject.error(msg)
+      assert_match(/ERROR -- : #{msg}\n/, fake_output.string)
+      assert_equal(msg, error)
+    end
+
+    it "#exception_message" do
+      exception = assert_raises(RuntimeError) do
+        subject.exception_message(msg)
+      end
+      assert_match(/WARN -- : #{msg}\n/, fake_output.string)
+      assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m", exception.message)
+    end
+
+    it "#success" do
+      subject.success(msg)
+      assert_match(/INFO -- : #{msg}\n/, fake_output.string)
+    end
+  end
+end
\ No newline at end of file
-- 
GitLab