require 'spec_helper'

describe "http compression" do
  let(:data)            { "uncompresseddata" }
  let(:response)        { double('response') }
  let(:compressed_zlib) { Zlib::Deflate.deflate(data) }
  let(:compressed_gzip) do
    str = StringIO.new
    writer = Zlib::GzipWriter.new(str)
    writer.write(data)
    writer.close.string
  end

  def stubs_response_with(response, content_encoding, body)
    allow(response).to receive(:[]).with('content-encoding').and_return(content_encoding)
    allow(response).to receive(:body).and_return(body)
  end

  describe "when zlib is not available" do
    before(:each) do
      allow(Puppet.features).to receive(:zlib?).and_return(false)

      require 'puppet/network/http/compression'
      class HttpUncompressor
        include Puppet::Network::HTTP::Compression::None
      end

      @uncompressor = HttpUncompressor.new
    end

    it "should have a module function that returns the None underlying module" do
      expect(Puppet::Network::HTTP::Compression.module).to eq(Puppet::Network::HTTP::Compression::None)
    end

    it "should not add any Accept-Encoding header" do
      expect(@uncompressor.add_accept_encoding({})).to eq({})
    end

    it "should not tamper the body" do
      response = double('response', :body => data)
      expect(@uncompressor.uncompress_body(response)).to eq(data)
    end

    it "should yield an identity uncompressor" do
      response = double('response')
      @uncompressor.uncompress(response) { |u|
        expect(u).to be_instance_of(Puppet::Network::HTTP::Compression::IdentityAdapter)
      }
    end
  end

  describe "when zlib is available" do
    require 'puppet/network/http/compression'
    class ActiveUncompressor
      include Puppet::Network::HTTP::Compression::Active
    end

    let(:uncompressor) { ActiveUncompressor.new }

    it "should have a module function that returns the Active underlying module" do
      expect(Puppet::Network::HTTP::Compression.module).to eq(Puppet::Network::HTTP::Compression::Active)
    end

    it "should add an Accept-Encoding header supporting compression" do
      headers = uncompressor.add_accept_encoding({})
      expect(headers).to have_key('accept-encoding')
      expect(headers['accept-encoding']).to match(/gzip/)
      expect(headers['accept-encoding']).to match(/deflate/)
      expect(headers['accept-encoding']).to match(/identity/)
    end

    describe "when uncompressing response body" do
      context "without compression" do
        it "should return untransformed response body with no content-encoding" do
          stubs_response_with(response, nil, data)

          expect(uncompressor.uncompress_body(response)).to eq(data)
        end

        it "should return untransformed response body with 'identity' content-encoding" do
          stubs_response_with(response, 'identity', data)

          expect(uncompressor.uncompress_body(response)).to eq(data)
        end
      end

      context "with 'zlib' content-encoding" do
        it "should use a Zlib inflater" do
          stubs_response_with(response, 'deflate', compressed_zlib)

          expect(uncompressor.uncompress_body(response)).to eq(data)
        end

      end

      context "with 'gzip' content-encoding" do
        it "should use a GzipReader" do
          stubs_response_with(response, 'gzip', compressed_gzip)

          expect(uncompressor.uncompress_body(response)).to eq(data)
        end

        it "should correctly decompress PSON containing UTF-8 in Binary Encoding" do
          # Simulate a compressed response body containing PSON containing UTF-8
          # using different UTF-8 widths:

          # \u06ff - ۿ - http://www.fileformat.info/info/unicode/char/06ff/index.htm - 0xDB 0xBF / 219 191
          # \u16A0 - ᚠ - http://www.fileformat.info/info/unicode/char/16A0/index.htm - 0xE1 0x9A 0xA0 / 225 154 160
          # \u{2070E} - 𠜎 - http://www.fileformat.info/info/unicode/char/2070E/index.htm - 0xF0 0xA0 0x9C 0x8E / 240 160 156 142

          pson = "foo\u06ff\u16A0\u{2070E}".to_pson # unicode expression eqivalent of "foo\xDB\xBF\xE1\x9A\xA0\xF0\xA0\x9C\x8E\" per above
          writer = Zlib::GzipWriter.new(StringIO.new)
          writer.write(pson)
          compressed_body = writer.close.string

          begin
            default_external = Encoding.default_external
            Encoding.default_external = Encoding::ISO_8859_1

            stubs_response_with(response, 'gzip', compressed_body)

            uncompressed = uncompressor.uncompress_body(response)
            # By default Zlib::GzipReader decompresses into Encoding.default_external, and we want to ensure our result is BINARY too
            expect(uncompressed.encoding).to eq(Encoding::BINARY)
            expect(uncompressed).to eq("\"foo\xDB\xBF\xE1\x9A\xA0\xF0\xA0\x9C\x8E\"".force_encoding(Encoding::BINARY))
          ensure
            Encoding.default_external = default_external
          end
        end
      end
    end

    describe "when uncompressing by chunk" do
      it "should yield an identity uncompressor with no content-encoding" do
        stubs_response_with(response, nil, data)

        expect { |b|
          uncompressor.uncompress(response).yield_once_with(Puppet::Network::HTTP::Compression::IdentityAdapter, &b)
        }
      end

      it "should yield an identity uncompressor with 'identity' content-encoding" do
        stubs_response_with(response, 'identity', data)

        expect { |b|
          uncompressor.uncompress(response).yield_once_with(Puppet::Network::HTTP::Compression::IdentityAdapter, &b)
        }
      end

      it "should yield a Zlib uncompressor with 'gzip' content-encoding" do
        stubs_response_with(response, 'gzip', compressed_gzip)

        expect { |b|
          uncompressor.uncompress(response).yield_once_with(Puppet::Network::HTTP::Compression::ZlibAdapter, &b)
        }
      end

      it "should yield a Zlib uncompressor with 'deflate' content-encoding" do
        stubs_response_with(response, 'deflate', compressed_zlib)

        expect { |b|
          uncompressor.uncompress(response).yield_once_with(Puppet::Network::HTTP::Compression::ZlibAdapter, &b)
        }
      end

      it "should close the underlying adapter" do
        stubs_response_with(response, 'identity', data)
        adapter = double('adapter')
        expect(Puppet::Network::HTTP::Compression::IdentityAdapter).to receive(:new).and_return(adapter)

        expect(adapter).to receive(:close)
        uncompressor.uncompress(response) { |u| }
      end

      it "should close the underlying adapter if the yielded block raises" do
        stubs_response_with(response, 'identity', data)
        adapter = double('adapter')
        expect(Puppet::Network::HTTP::Compression::IdentityAdapter).to receive(:new).and_return(adapter)

        expect(adapter).to receive(:close)
        expect {
          uncompressor.uncompress(response) { |u| raise ArgumentError, "whoops" }
        }.to raise_error(ArgumentError, "whoops")
      end
    end

    describe "zlib adapter" do
      it "should initialize the underlying inflater with gzip/zlib header parsing" do
        expect(Zlib::Inflate).to receive(:new).with(15 + 32)

        Puppet::Network::HTTP::Compression::Active::ZlibAdapter.new
      end

      it "should return the given chunk" do
        adapter = Puppet::Network::HTTP::Compression::Active::ZlibAdapter.new

        expect(adapter.uncompress(compressed_zlib)).to eq(data)
      end

      it "should try a 'regular' inflater on Zlib::DataError" do
        inflater = Zlib::Inflate.new(15 + 32)
        expect(inflater).to receive(:inflate).and_raise(Zlib::DataError.new("not a zlib stream"))
        adapter = Puppet::Network::HTTP::Compression::Active::ZlibAdapter.new(inflater)

        expect(adapter.uncompress(compressed_zlib)).to eq(data)
      end

      it "should raise the error the second time" do
        inflater = Zlib::Inflate.new(15 + 32)
        expect(inflater).to receive(:inflate).and_raise(Zlib::DataError.new("not a zlib stream"))
        adapter = Puppet::Network::HTTP::Compression::Active::ZlibAdapter.new(inflater)

        expect { adapter.uncompress("this is not compressed data") }.to raise_error(Zlib::DataError, /incorrect header check/)
      end

      it "should finish and close the stream" do
        inflater = double('inflater')
        expect(inflater).to receive(:finish)
        expect(inflater).to receive(:close)
        adapter = Puppet::Network::HTTP::Compression::Active::ZlibAdapter.new(inflater)

        adapter.close
      end

      it "should close the stream even if finish raises" do
        inflater = double('inflater')
        expect(inflater).to receive(:finish).and_raise(Zlib::BufError)
        expect(inflater).to receive(:close)

        adapter = Puppet::Network::HTTP::Compression::Active::ZlibAdapter.new(inflater)
        expect {
          adapter.close
        }.to raise_error(Zlib::BufError)
      end
    end
  end
end
