require 'spec_helper' require 'fileutils' describe PuppetForge::V3::Release do context 'connection management' do before(:each) do PuppetForge::Connection.authorization = nil PuppetForge::Connection.proxy = nil described_class.conn = PuppetForge::V3::Base.conn(true) end after(:each) do PuppetForge::Connection.authorization = nil PuppetForge::Connection.proxy = nil described_class.conn = nil end describe 'setting authorization value after a connection is created' do it 'should reset connection' do old_conn = described_class.conn PuppetForge::Connection.authorization = 'post-init auth value' new_conn = described_class.conn expect(new_conn).to_not eq(old_conn) expect(new_conn.headers).to include(:authorization => 'post-init auth value') end end describe 'setting proxy value after a connection is created' do it 'should reset connection' do old_conn = described_class.conn PuppetForge::Connection.proxy = 'http://proxy.example.com:8888' new_conn = described_class.conn expect(new_conn).to_not eq(old_conn) expect(new_conn.proxy).to_not be_nil expect(new_conn.proxy.uri.to_s).to eq('http://proxy.example.com:8888') end end end context 'with stubbed connection' do before do stub_api_for(PuppetForge::V3::Base) do |stubs| stub_fixture(stubs, :get, '/v3/releases/puppetlabs-apache-0.0.1') stub_fixture(stubs, :get, '/v3/releases/absent-apache-0.0.1') stub_fixture(stubs, :get, '/v3/files/puppetlabs-apache-0.0.1.tar.gz') stub_fixture(stubs, :get, '/v3/modules/puppetlabs-apache') stub_fixture(stubs, :get, '/v3/releases?module=puppetlabs-apache') end end describe '::find' do let(:release) { PuppetForge::V3::Release.find('puppetlabs-apache-0.0.1') } let(:missing_release) { PuppetForge::V3::Release.find('absent-apache-0.0.1') } it 'can find releases that exist' do expect(release.version).to eql('0.0.1') end it 'raises Faraday::ResourceNotFound for non-existent releases' do expect { missing_release }.to raise_error(Faraday::ResourceNotFound) end end describe '#module' do let(:release) { PuppetForge::V3::Release.find('puppetlabs-apache-0.0.1') } it 'exposes the related module as a property' do expect(release.module).to_not be nil end it 'grants access to module attributes without an API call' do expect(PuppetForge::V3::Module).not_to receive(:request) expect(release.module.name).to eql('apache') end it 'transparently makes API calls for other attributes' do expect(PuppetForge::V3::Module).to receive(:request).once.and_call_original expect(release.module.created_at).to_not be nil end end describe '#download_url' do let(:release) { PuppetForge::V3::Release.find('puppetlabs-apache-0.0.1') } it 'handles an API response that does not include a scheme and host' do release.file_uri = '/v3/files/puppetlabs-apache-0.0.1.tar.gz' uri_with_host = URI.join(PuppetForge.host, '/v3/files/puppetlabs-apache-0.0.1.tar.gz').to_s expect(release.download_url).to eql(uri_with_host) end it 'handles an API response that includes a scheme and host' do release.file_uri = 'https://example.com/v3/files/puppetlabs-apache-0.0.1.tar.gz' expect(release.download_url).to eql('https://example.com/v3/files/puppetlabs-apache-0.0.1.tar.gz') end context 'when PuppetForge.host has a path prefix' do around(:each) do |spec| old_host = PuppetForge.host PuppetForge.host = 'http://example.com/forge/api/' spec.run PuppetForge.host = old_host end it 'includes path prefix in download url' do expect(release.download_url).to eql('http://example.com/forge/api/v3/files/puppetlabs-apache-0.0.1.tar.gz') end end end describe '#download' do let(:release) { PuppetForge::V3::Release.find('puppetlabs-apache-0.0.1') } let(:tarball) { "#{PROJECT_ROOT}/spec/tmp/module.tgz" } before { FileUtils.rm tarball rescue nil } after { FileUtils.rm tarball rescue nil } it 'downloads the file to the specified location' do expect(File.exist?(tarball)).to be false release.download(Pathname.new(tarball)) expect(File.exist?(tarball)).to be true end context 'when response is 403' do it "raises PuppetForge::ReleaseForbidden" do mock_conn = instance_double("PuppetForge::V3::Connection", :url_prefix => PuppetForge.host) allow(described_class).to receive(:conn).and_return(mock_conn) expect(mock_conn).to receive(:get).and_raise(Faraday::ClientError.new("403", {:status => 403, :body => ({:message => "Forbidden"}.to_json)})) expect { release.download(Pathname.new(tarball)) }.to raise_error(PuppetForge::ReleaseForbidden) end end context 'when connection fails' do it "re-raises original error" do mock_conn = instance_double("PuppetForge::V3::Connection", :url_prefix => PuppetForge.host) allow(described_class).to receive(:conn).and_return(mock_conn) expect(mock_conn).to receive(:get).and_raise(Faraday::ConnectionFailed.new("connection failed")) expect { release.download(Pathname.new(tarball)) }.to raise_error(Faraday::ConnectionFailed, /connection failed/) end end end describe '#verify' do let(:release) { PuppetForge::V3::Release.find('puppetlabs-apache-0.0.1') } let(:tarball) { "#{PROJECT_ROOT}/spec/tmp/module.tgz" } let(:allow_md5) { true } before(:each) do FileUtils.rm tarball rescue nil release.download(Pathname.new(tarball)) end after(:each) { FileUtils.rm tarball rescue nil } context 'file_sha256 is available' do before(:each) do allow(release).to receive(:file_sha256).and_return("810ff2fb242a5dee4220f2cb0e6a519891fb67f2f828a6cab4ef8894633b1f50") end let(:mock_sha256) { double(Digest::SHA256, hexdigest: release.file_sha256) } it 'only verifies sha-256 checksum' do expect(Digest::SHA256).to receive(:file).and_return(mock_sha256) expect(Digest::MD5).not_to receive(:file) release.verify(tarball, allow_md5) end end context 'file_sha256 is not available' do let(:mock_md5) { double(Digest::MD5, hexdigest: release.file_md5) } it 'only verfies the md5 checksum' do expect(Digest::SHA256).not_to receive(:file) expect(Digest::MD5).to receive(:file).and_return(mock_md5) release.verify(tarball, allow_md5) end end context 'when allow_md5=false' do let(:allow_md5) { false } context 'file_sha256 is not available' do it 'raises an appropriate error' do expect(Digest::SHA256).not_to receive(:file) expect(Digest::MD5).not_to receive(:file) expect { release.verify(tarball, allow_md5) }.to raise_error(PuppetForge::Error, /cannot verify module release.*md5.*forbidden/i) end end end end describe '#metadata' do let(:release) { PuppetForge::V3::Release.find('puppetlabs-apache-0.0.1') } it 'is lazy and repeatable' do 3.times do expect(release.module.releases.last.metadata).to_not be_nil end end end describe 'instance properies' do let(:release) { PuppetForge::V3::Release.find('puppetlabs-apache-0.0.1') } example 'are easily accessible' do expect(release.created_at).to_not be nil end end describe '#upload' do let(:tarball) { "#{PROJECT_ROOT}/spec/tmp/module.tgz" } let(:file_object) { double('file', read: 'file contents') } let(:release) { PuppetForge::V3::Release.upload(tarball) } let(:mock_conn) { instance_double('PuppetForge::V3::Connection', url_prefix: PuppetForge.host) } context 'when there is no auth token provided' do it 'raises PuppetForge::ReleaseForbidden' do allow(File).to receive(:file?).and_return(true) allow(File).to receive(:open).and_return(file_object) allow(described_class).to receive(:conn).and_return(mock_conn) response = { status: 403, body: { 'message' => 'Forbidden' }.to_json } expect(mock_conn).to receive(:post).and_raise(Faraday::ClientError.new('Forbidden', response)) expect { release }.to raise_error(PuppetForge::ReleaseForbidden) end end context 'when the module is not valid' do it 'raises PuppetForge::ReleaseBadRequest' do allow(File).to receive(:file?).and_return(true) allow(File).to receive(:open).and_return(file_object) allow(described_class).to receive(:conn).and_return(mock_conn) response = { status: 400, body: { message: 'Bad Content' }.to_json } expect(mock_conn).to receive(:post).and_raise(Faraday::ClientError.new('400', response)) expect { release }.to raise_error(PuppetForge::ReleaseBadContent) end end context 'when the tarball does not exist' do it 'raises PuppetForge::FileNotFound' do expect { PuppetForge::V3::Release.upload(tarball) }.to raise_error(PuppetForge::FileNotFound) end end end end end