require 'spec_helper'

describe R10K::Git::Rugged::Credentials, :unless => R10K::Util::Platform.jruby?  || R10K::Util::Platform.windows? do
  before(:all) do
    require 'r10k/git/rugged/credentials'
    require 'rugged/credentials'
  end

  let(:repo) { R10K::Git::Rugged::BareRepository.new("/some/nonexistent/path", "repo.git") }

  subject { described_class.new(repo) }

  after(:each) { R10K::Git.settings.reset! }

  describe "determining the username" do
    before { R10K::Git.settings[:username] = "moderns" }
    after { R10K::Git.settings.reset! }

    it "prefers a username from the URL" do
      user = subject.get_git_username("https://tessier-ashpool.freeside/repo.git", "ashpool")
      expect(user).to eq "ashpool"
    end

    it "uses the username from the Git config when specified" do
      user = subject.get_git_username("https://tessier-ashpool.freeside/repo.git", nil)
      expect(user).to eq "moderns"
    end

    it "falls back to the current user" do
      R10K::Git.settings.reset!
      expect(Etc).to receive(:getlogin).and_return("finn")
      user = subject.get_git_username("https://tessier-ashpool.freeside/repo.git", nil)
      expect(user).to eq "finn"
    end
  end

  describe "generating ssh key credentials" do
    after(:each) { R10K::Git.settings.reset! }

    it "prefers a per-repository SSH private key" do
      allow(File).to receive(:readable?).with("/etc/puppetlabs/r10k/ssh/tessier-ashpool-id_rsa").and_return true
      R10K::Git.settings[:private_key] = "/etc/puppetlabs/r10k/ssh/id_rsa"
      R10K::Git.settings[:repositories] = [{ remote: "ssh://git@tessier-ashpool.freeside/repo.git",
        private_key: "/etc/puppetlabs/r10k/ssh/tessier-ashpool-id_rsa"}]
      creds = subject.get_ssh_key_credentials("ssh://git@tessier-ashpool.freeside/repo.git", nil)
      expect(creds).to be_a_kind_of(Rugged::Credentials::SshKey)
      expect(creds.instance_variable_get(:@privatekey)).to eq("/etc/puppetlabs/r10k/ssh/tessier-ashpool-id_rsa")
    end

    it "falls back to the global SSH private key" do
      allow(File).to receive(:readable?).with("/etc/puppetlabs/r10k/ssh/id_rsa").and_return true
      R10K::Git.settings[:private_key] = "/etc/puppetlabs/r10k/ssh/id_rsa"
      creds = subject.get_ssh_key_credentials("ssh://git@tessier-ashpool.freeside/repo.git", nil)
      expect(creds).to be_a_kind_of(Rugged::Credentials::SshKey)
      expect(creds.instance_variable_get(:@privatekey)).to eq("/etc/puppetlabs/r10k/ssh/id_rsa")
    end

    it "raises an error if no key has been set" do
      R10K::Git.settings[:private_key] = nil
      expect {
        subject.get_ssh_key_credentials("https://tessier-ashpool.freeside/repo.git", nil)
      }.to raise_error(R10K::Git::GitError, /no private key was given/)
    end

    it "raises an error if the private key is unreadable" do
      R10K::Git.settings[:private_key] = "/some/nonexistent/.ssh/key"
      expect(File).to receive(:readable?).with("/some/nonexistent/.ssh/key").and_return false
      expect {
        subject.get_ssh_key_credentials("https://tessier-ashpool.freeside/repo.git", nil)
      }.to raise_error(R10K::Git::GitError, /Unable to use SSH key auth for.*is missing or unreadable/)
    end

    it "generates the rugged sshkey credential type" do
      allow(File).to receive(:readable?).with("/etc/puppetlabs/r10k/ssh/id_rsa").and_return true
      R10K::Git.settings[:private_key] = "/etc/puppetlabs/r10k/ssh/id_rsa"
      creds = subject.get_ssh_key_credentials("https://tessier-ashpool.freeside/repo.git", nil)
      expect(creds).to be_a_kind_of(Rugged::Credentials::SshKey)
      expect(creds.instance_variable_get(:@privatekey)).to eq("/etc/puppetlabs/r10k/ssh/id_rsa")
    end
  end

  describe "generating github app tokens" do
    it 'errors if app id has invalid characters' do
      expect { subject.github_app_token("123A567890", "fake", "300")
      }.to raise_error(R10K::Git::GitError, /App id contains invalid characters/)
    end
    it 'errors if app ttl has invalid characters' do
      expect { subject.github_app_token("123456", "fake", "abc")
      }.to raise_error(R10K::Git::GitError, /Github App token ttl contains/)
    end
    it 'errors if private file does not exist' do
      R10K::Git.settings[:github_app_key] = "/missing/token/file"
      expect(File).to receive(:readable?).with(R10K::Git.settings[:github_app_key]).and_return false
      expect {
        subject.github_app_token("123456", R10K::Git.settings[:github_app_key], "300")
      }.to raise_error(R10K::Git::GitError, /App key is missing or unreadable/)
    end
    it 'errors if file is not a valid SSL key' do
      token_file = Tempfile.new('token')
      token_file.write('my_token')
      token_file.close
      R10K::Git.settings[:github_app_key] = token_file.path
      expect(File).to receive(:readable?).with(token_file.path).and_return true
      expect {
        subject.github_app_token("123456", R10K::Git.settings[:github_app_key], "300")
      }.to raise_error(R10K::Git::GitError, /App key is not a valid SSL key/)
      token_file.unlink
    end
  end

  describe "generating token credentials" do
    it 'errors if token file does not exist' do
      R10K::Git.settings[:oauth_token] = "/missing/token/file"
      expect(File).to receive(:readable?).with("/missing/token/file").and_return false
      R10K::Git.settings[:repositories] = [{remote: "https://tessier-ashpool.freeside/repo.git"}]
      expect {
        subject.get_plaintext_credentials("https://tessier-ashpool.freeside/repo.git", nil)
      }.to raise_error(R10K::Git::GitError, /cannot load OAuth token/)
    end

    it 'errors if the token on stdin is not a valid OAuth token' do
      allow($stdin).to receive(:read).and_return("<bad>token")
      R10K::Git.settings[:oauth_token] = "-"
      R10K::Git.settings[:repositories] = [{remote: "https://tessier-ashpool.freeside/repo.git"}]
      expect {
        subject.get_plaintext_credentials("https://tessier-ashpool.freeside/repo.git", nil)
      }.to raise_error(R10K::Git::GitError, /invalid characters/)
    end

    it 'errors if the token in the file is not a valid OAuth token' do
      token_file = Tempfile.new('token')
      token_file.write('my bad \ntoken')
      token_file.close
      R10K::Git.settings[:oauth_token] = token_file.path
      R10K::Git.settings[:repositories] = [{remote: "https://tessier-ashpool.freeside/repo.git"}]
      expect {
        subject.get_plaintext_credentials("https://tessier-ashpool.freeside/repo.git", nil)
      }.to raise_error(R10K::Git::GitError, /invalid characters/)
    end

    it 'prefers per-repo token file' do
      token_file = Tempfile.new('token')
      token_file.write('my_token')
      token_file.close
      R10K::Git.settings[:oauth_token] = "/do/not/use"
      R10K::Git.settings[:repositories] = [{remote: "https://tessier-ashpool.freeside/repo.git",
                                            oauth_token: token_file.path }]
      creds = subject.get_plaintext_credentials("https://tessier-ashpool.freeside/repo.git", nil)
      expect(creds).to be_a_kind_of(Rugged::Credentials::UserPassword)
      expect(creds.instance_variable_get(:@password)).to eq("my_token")
      expect(creds.instance_variable_get(:@username)).to eq("x-oauth-token")
    end

    it 'uses the token from a file as a password' do
      token_file = Tempfile.new('token')
      token_file.write('my_token')
      token_file.close
      R10K::Git.settings[:oauth_token] = token_file.path
      R10K::Git.settings[:repositories] = [{remote: "https://tessier-ashpool.freeside/repo.git"}]
      creds = subject.get_plaintext_credentials("https://tessier-ashpool.freeside/repo.git", nil)
      expect(creds).to be_a_kind_of(Rugged::Credentials::UserPassword)
      expect(creds.instance_variable_get(:@password)).to eq("my_token")
      expect(creds.instance_variable_get(:@username)).to eq("x-oauth-token")
    end

    it 'uses the token from stdin as a password' do
      allow($stdin).to receive(:read).and_return("my_token")
      R10K::Git.settings[:oauth_token] = '-'
      R10K::Git.settings[:repositories] = [{remote: "https://tessier-ashpool.freeside/repo.git"}]
      creds = subject.get_plaintext_credentials("https://tessier-ashpool.freeside/repo.git", nil)
      expect(creds).to be_a_kind_of(Rugged::Credentials::UserPassword)
      expect(creds.instance_variable_get(:@password)).to eq("my_token")
      expect(creds.instance_variable_get(:@username)).to eq("x-oauth-token")
    end

    it 'only reads the token in once' do
      expect($stdin).to receive(:read).and_return("my_token").once
      R10K::Git.settings[:oauth_token] = '-'
      R10K::Git.settings[:repositories] = [{remote: "https://tessier-ashpool.freeside/repo.git"}]
      creds = subject.get_plaintext_credentials("https://tessier-ashpool.freeside/repo.git", nil)
      expect(creds.instance_variable_get(:@password)).to eq("my_token")
      creds = subject.get_plaintext_credentials("https://tessier-ashpool.freeside/repo.git", nil)
      expect(creds.instance_variable_get(:@password)).to eq("my_token")
    end
  end

  describe "generating default credentials" do
    it "generates the rugged default credential type" do
      creds = subject.get_default_credentials("https://azurediamond:hunter2@tessier-ashpool.freeside/repo.git", "azurediamond")
      expect(creds).to be_a_kind_of(Rugged::Credentials::Default)
    end
  end

  describe "generating credentials" do
    it "creates ssh key credentials for the sshkey allowed type" do
      allow(File).to receive(:readable?).with("/etc/puppetlabs/r10k/ssh/id_rsa").and_return true
      R10K::Git.settings[:private_key] = "/etc/puppetlabs/r10k/ssh/id_rsa"
      expect(subject.call("https://tessier-ashpool.freeside/repo.git", nil, [:ssh_key])).to be_a_kind_of(Rugged::Credentials::SshKey)
    end

    it "creates user/password credentials for the default allowed type" do
      expect(subject.call("https://tessier-ashpool.freeside/repo.git", nil, [:plaintext])).to be_a_kind_of(Rugged::Credentials::UserPassword)
    end

    it "creates default credentials when no other types are allowed" do
      expect(subject.call("https://tessier-ashpool.freeside/repo.git", nil, [])).to be_a_kind_of(Rugged::Credentials::Default)
    end

    it "refuses to generate credentials more than 50 times" do
      (1..50).each { subject.call("https://tessier-ashpool.freeside/repo.git", nil, [:plaintext]) }

      expect { subject.call("https://tessier-ashpool.freeside/repo.git", nil, [:plaintext]) }.to raise_error(R10K::Git::GitError, /authentication failed/i)
    end
  end
end
