shell_spec.rb 14.4 KB
Newer Older
1
require 'spec_helper'
2
require 'stringio'
3

4
describe Gitlab::Shell do
5 6
  set(:project) { create(:project, :repository) }

7
  let(:gitlab_shell) { described_class.new }
8
  let(:popen_vars) { { 'GIT_TERMINAL_PROMPT' => ENV['GIT_TERMINAL_PROMPT'] } }
9 10
  let(:gitlab_projects) { double('gitlab_projects') }
  let(:timeout) { Gitlab.config.gitlab_shell.git_timeout }
11 12

  before do
13
    allow(Project).to receive(:find).and_return(project)
14 15 16 17

    allow(gitlab_shell).to receive(:gitlab_projects)
      .with(project.repository_storage_path, project.disk_path + '.git')
      .and_return(gitlab_projects)
18 19
  end

20 21 22 23 24
  it { is_expected.to respond_to :add_key }
  it { is_expected.to respond_to :remove_key }
  it { is_expected.to respond_to :add_repository }
  it { is_expected.to respond_to :remove_repository }
  it { is_expected.to respond_to :fork_repository }
25

26
  it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") }
27

28
  describe 'memoized secret_token' do
29 30 31 32 33
    let(:secret_file) { 'tmp/tests/.secret_shell_test' }
    let(:link_file) { 'tmp/tests/shell-secret-test/.gitlab_shell_secret' }

    before do
      allow(Gitlab.config.gitlab_shell).to receive(:secret_file).and_return(secret_file)
34
      allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-secret-test')
35
      FileUtils.mkdir('tmp/tests/shell-secret-test')
36
      described_class.ensure_secret_token!
37 38 39 40 41 42 43 44
    end

    after do
      FileUtils.rm_rf('tmp/tests/shell-secret-test')
      FileUtils.rm_rf(secret_file)
    end

    it 'creates and links the secret token file' do
45
      secret_token = described_class.secret_token
46

47
      expect(File.exist?(secret_file)).to be(true)
48
      expect(File.read(secret_file).chomp).to eq(secret_token)
49 50 51 52 53
      expect(File.symlink?(link_file)).to be(true)
      expect(File.readlink(link_file)).to eq(secret_file)
    end
  end

54
  describe Gitlab::Shell::KeyAdder do
55
    describe '#add_key' do
56 57
      it 'removes trailing garbage' do
        io = spy(:io)
58 59
        adder = described_class.new(io)

60 61 62 63 64
        adder.add_key('key-42', "ssh-rsa foo bar\tbaz")

        expect(io).to have_received(:puts).with("key-42\tssh-rsa foo")
      end

65 66 67 68 69 70 71 72 73
      it 'handles multiple spaces in the key' do
        io = spy(:io)
        adder = described_class.new(io)

        adder.add_key('key-42', "ssh-rsa  foo")

        expect(io).to have_received(:puts).with("key-42\tssh-rsa foo")
      end

74 75 76 77 78
      it 'raises an exception if the key contains a tab' do
        expect do
          described_class.new(StringIO.new).add_key('key-42', "ssh-rsa\tfoobar")
        end.to raise_error(Gitlab::Shell::Error)
      end
79

80 81 82 83
      it 'raises an exception if the key contains a newline' do
        expect do
          described_class.new(StringIO.new).add_key('key-42', "ssh-rsa foobar\nssh-rsa pawned")
        end.to raise_error(Gitlab::Shell::Error)
84 85 86
      end
    end
  end
87 88

  describe 'projects commands' do
89 90 91
    let(:gitlab_shell_path) { File.expand_path('tmp/tests/gitlab-shell') }
    let(:projects_path) { File.join(gitlab_shell_path, 'bin/gitlab-projects') }
    let(:gitlab_shell_hooks_path) { File.join(gitlab_shell_path, 'hooks') }
92 93

    before do
94 95
      allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(gitlab_shell_path)
      allow(Gitlab.config.gitlab_shell).to receive(:hooks_path).and_return(gitlab_shell_hooks_path)
96 97 98
      allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800)
    end

99 100 101 102 103 104 105 106 107 108 109
    describe '#add_key' do
      it 'removes trailing garbage' do
        allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
        expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
          [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar']
        )

        gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
      end
    end

110
    describe '#add_repository' do
J
Jacob Vosmaer 已提交
111 112 113 114 115
      shared_examples '#add_repository' do
        let(:repository_storage) { 'default' }
        let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
        let(:repo_name) { 'project/path' }
        let(:created_path) { File.join(repository_storage_path, repo_name + '.git') }
116

J
Jacob Vosmaer 已提交
117
        after do
118 119 120
          FileUtils.rm_rf(created_path)
        end

J
Jacob Vosmaer 已提交
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
        it 'creates a repository' do
          expect(gitlab_shell.add_repository(repository_storage, repo_name)).to be_truthy

          expect(File.stat(created_path).mode & 0o777).to eq(0o770)

          hooks_path = File.join(created_path, 'hooks')
          expect(File.lstat(hooks_path)).to be_symlink
          expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path)
        end

        it 'returns false when the command fails' do
          FileUtils.mkdir_p(File.dirname(created_path))
          # This file will block the creation of the repo's .git directory. That
          # should cause #add_repository to fail.
          FileUtils.touch(created_path)

          expect(gitlab_shell.add_repository(repository_storage, repo_name)).to be_falsy
        end
139 140
      end

J
Jacob Vosmaer 已提交
141
      context 'with gitaly' do
J
Jacob Vosmaer 已提交
142 143
        it_behaves_like '#add_repository'
      end
144

145
      context 'without gitaly', :skip_gitaly_mock do
J
Jacob Vosmaer 已提交
146
        it_behaves_like '#add_repository'
147 148 149 150
      end
    end

    describe '#remove_repository' do
151 152
      subject { gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path) }

153
      it 'returns true when the command succeeds' do
154
        expect(gitlab_projects).to receive(:rm_project) { true }
155

156
        is_expected.to be_truthy
157 158 159
      end

      it 'returns false when the command fails' do
160
        expect(gitlab_projects).to receive(:rm_project) { false }
161

162
        is_expected.to be_falsy
163 164 165 166 167
      end
    end

    describe '#mv_repository' do
      it 'returns true when the command succeeds' do
168
        expect(gitlab_projects).to receive(:mv_project).with('project/newpath.git') { true }
169

170
        expect(gitlab_shell.mv_repository(project.repository_storage_path, project.disk_path, 'project/newpath')).to be_truthy
171 172 173
      end

      it 'returns false when the command fails' do
174
        expect(gitlab_projects).to receive(:mv_project).with('project/newpath.git') { false }
175

176
        expect(gitlab_shell.mv_repository(project.repository_storage_path, project.disk_path, 'project/newpath')).to be_falsy
177 178 179 180
      end
    end

    describe '#fork_repository' do
181 182 183 184 185 186 187 188 189
      subject do
        gitlab_shell.fork_repository(
          project.repository_storage_path,
          project.disk_path,
          'new/storage',
          'fork/path'
        )
      end

190
      it 'returns true when the command succeeds' do
191
        expect(gitlab_projects).to receive(:fork_repository).with('new/storage', 'fork/path.git') { true }
192

193
        is_expected.to be_truthy
194 195 196
      end

      it 'return false when the command fails' do
197
        expect(gitlab_projects).to receive(:fork_repository).with('new/storage', 'fork/path.git') { false }
198

199
        is_expected.to be_falsy
200 201 202
      end
    end

203
    shared_examples 'fetch_remote' do |gitaly_on|
204
      let(:repository) { project.repository }
205

206
      def fetch_remote(ssh_auth = nil)
207
        gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', ssh_auth: ssh_auth)
208 209
      end

210 211 212 213 214 215
      def expect_gitlab_projects(fail = false, options = {})
        expect(gitlab_projects).to receive(:fetch_remote).with(
          'remote-name',
          timeout,
          options
        ).and_return(!fail)
216

217
        allow(gitlab_projects).to receive(:output).and_return('error') if fail
218 219
      end

220
      def expect_gitaly_call(fail, options = {})
221 222 223 224 225 226 227 228 229 230 231
        receive_fetch_remote =
          if fail
            receive(:fetch_remote).and_raise(GRPC::NotFound)
          else
            receive(:fetch_remote).and_return(true)
          end

        expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive_fetch_remote
      end

      if gitaly_on
232 233
        def expect_call(fail, options = {})
          expect_gitaly_call(fail, options)
234 235
        end
      else
236 237
        def expect_call(fail, options = {})
          expect_gitlab_projects(fail, options)
238
        end
239 240 241 242 243 244 245 246 247 248 249 250 251
      end

      def build_ssh_auth(opts = {})
        defaults = {
          ssh_import?: true,
          ssh_key_auth?: false,
          ssh_known_hosts: nil,
          ssh_private_key: nil
        }

        double(:ssh_auth, defaults.merge(opts))
      end

252
      it 'returns true when the command succeeds' do
253
        expect_call(false, force: false, tags: true)
254

255
        expect(fetch_remote).to be_truthy
256 257 258
      end

      it 'raises an exception when the command fails' do
259
        expect_call(true, force: false, tags: true)
260

261
        expect { fetch_remote }.to raise_error(Gitlab::Shell::Error)
262 263
      end

264 265 266 267 268 269 270
      it 'allows forced and no_tags to be changed' do
        expect_call(false, force: true, tags: false)

        result = gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', forced: true, no_tags: true)
        expect(result).to be_truthy
      end

271 272
      context 'SSH auth' do
        it 'passes the SSH key if specified' do
273
          expect_call(false, force: false, tags: true, ssh_key: 'foo')
274 275 276 277 278 279 280

          ssh_auth = build_ssh_auth(ssh_key_auth?: true, ssh_private_key: 'foo')

          expect(fetch_remote(ssh_auth)).to be_truthy
        end

        it 'does not pass an empty SSH key' do
281
          expect_call(false, force: false, tags: true)
282 283 284 285 286 287 288

          ssh_auth = build_ssh_auth(ssh_key_auth: true, ssh_private_key: '')

          expect(fetch_remote(ssh_auth)).to be_truthy
        end

        it 'does not pass the key unless SSH key auth is to be used' do
289
          expect_call(false, force: false, tags: true)
290 291 292 293 294 295 296

          ssh_auth = build_ssh_auth(ssh_key_auth: false, ssh_private_key: 'foo')

          expect(fetch_remote(ssh_auth)).to be_truthy
        end

        it 'passes the known_hosts data if specified' do
297
          expect_call(false, force: false, tags: true, known_hosts: 'foo')
298 299 300 301 302 303 304

          ssh_auth = build_ssh_auth(ssh_known_hosts: 'foo')

          expect(fetch_remote(ssh_auth)).to be_truthy
        end

        it 'does not pass empty known_hosts data' do
305
          expect_call(false, force: false, tags: true)
306 307 308 309 310 311 312

          ssh_auth = build_ssh_auth(ssh_known_hosts: '')

          expect(fetch_remote(ssh_auth)).to be_truthy
        end

        it 'does not pass known_hosts data unless SSH is to be used' do
313
          expect_call(false, force: false, tags: true)
314 315

          ssh_auth = build_ssh_auth(ssh_import?: false, ssh_known_hosts: 'foo')
316

317 318
          expect(fetch_remote(ssh_auth)).to be_truthy
        end
319 320 321
      end
    end

322
    describe '#fetch_remote local', :skip_gitaly_mock do
323 324 325 326 327 328 329
      it_should_behave_like 'fetch_remote', false
    end

    describe '#fetch_remote gitaly' do
      it_should_behave_like 'fetch_remote', true
    end

330
    describe '#import_repository' do
331 332
      let(:import_url) { 'https://gitlab.com/gitlab-org/gitlab-ce.git' }

333
      it 'returns true when the command succeeds' do
334
        expect(gitlab_projects).to receive(:import_project).with(import_url, timeout) { true }
335

336 337 338
        result = gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, import_url)

        expect(result).to be_truthy
339 340 341
      end

      it 'raises an exception when the command fails' do
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
        allow(gitlab_projects).to receive(:output) { 'error' }
        expect(gitlab_projects).to receive(:import_project) { false }

        expect do
          gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, import_url)
        end.to raise_error(Gitlab::Shell::Error, "error")
      end
    end

    describe '#push_remote_branches' do
      subject(:result) do
        gitlab_shell.push_remote_branches(
          project.repository_storage_path,
          project.disk_path,
          'downstream-remote',
          ['master']
        )
      end

      it 'executes the command' do
        expect(gitlab_projects).to receive(:push_branches)
          .with('downstream-remote', timeout, true, ['master'])
          .and_return(true)

        is_expected.to be_truthy
      end

      it 'fails to execute the command' do
        allow(gitlab_projects).to receive(:output) { 'error' }
        expect(gitlab_projects).to receive(:push_branches)
          .with('downstream-remote', timeout, true, ['master'])
          .and_return(false)

        expect { result }.to raise_error(Gitlab::Shell::Error, 'error')
      end
    end

    describe '#delete_remote_branches' do
      subject(:result) do
        gitlab_shell.delete_remote_branches(
          project.repository_storage_path,
          project.disk_path,
          'downstream-remote',
          ['master']
        )
      end

      it 'executes the command' do
        expect(gitlab_projects).to receive(:delete_remote_branches)
          .with('downstream-remote', ['master'])
          .and_return(true)

        is_expected.to be_truthy
      end

      it 'fails to execute the command' do
        allow(gitlab_projects).to receive(:output) { 'error' }
        expect(gitlab_projects).to receive(:delete_remote_branches)
          .with('downstream-remote', ['master'])
          .and_return(false)
402

403
        expect { result }.to raise_error(Gitlab::Shell::Error, 'error')
404 405 406
      end
    end
  end
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454

  describe 'namespace actions' do
    subject { described_class.new }
    let(:storage_path) { Gitlab.config.repositories.storages.default.path }

    describe '#add_namespace' do
      it 'creates a namespace' do
        subject.add_namespace(storage_path, "mepmep")

        expect(subject.exists?(storage_path, "mepmep")).to be(true)
      end
    end

    describe '#exists?' do
      context 'when the namespace does not exist' do
        it 'returns false' do
          expect(subject.exists?(storage_path, "non-existing")).to be(false)
        end
      end

      context 'when the namespace exists' do
        it 'returns true' do
          subject.add_namespace(storage_path, "mepmep")

          expect(subject.exists?(storage_path, "mepmep")).to be(true)
        end
      end
    end

    describe '#remove' do
      it 'removes the namespace' do
        subject.add_namespace(storage_path, "mepmep")
        subject.rm_namespace(storage_path, "mepmep")

        expect(subject.exists?(storage_path, "mepmep")).to be(false)
      end
    end

    describe '#mv_namespace' do
      it 'renames the namespace' do
        subject.add_namespace(storage_path, "mepmep")
        subject.mv_namespace(storage_path, "mepmep", "2mep")

        expect(subject.exists?(storage_path, "mepmep")).to be(false)
        expect(subject.exists?(storage_path, "2mep")).to be(true)
      end
    end
  end
455
end