From c4db541c1b2c97ab1eda354ea3899489fe5c33e5 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 24 Mar 2020 21:07:54 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- spec/frontend/blob/blob_file_dropzone_spec.js | 7 +- spec/frontend/boards/board_new_issue_spec.js | 8 +- .../diffs/components/diff_file_header_spec.js | 6 - spec/frontend/helpers/jquery.js | 12 + spec/frontend/mocks/node/jquery.js | 15 -- spec/frontend/notes/components/notes_app_spec.js | 2 +- .../releases/stores/modules/detail/actions_spec.js | 19 +- .../stores/modules/detail/mutations_spec.js | 77 +++---- spec/frontend/test_setup.js | 9 +- .../vue_shared/components/file_row_spec.js | 13 -- spec/lib/gitlab/database/migration_helpers_spec.rb | 44 +++- spec/lib/gitlab/x509/commit_spec.rb | 244 +-------------------- spec/lib/gitlab/x509/signature_spec.rb | 232 ++++++++++++++++++++ spec/requests/api/pipeline_schedules_spec.rb | 2 +- spec/support/helpers/x509_helpers.rb | 4 + 15 files changed, 349 insertions(+), 345 deletions(-) delete mode 100644 spec/frontend/mocks/node/jquery.js create mode 100644 spec/lib/gitlab/x509/signature_spec.rb (limited to 'spec') diff --git a/spec/frontend/blob/blob_file_dropzone_spec.js b/spec/frontend/blob/blob_file_dropzone_spec.js index 4e9a05418df..cbd36abd4ff 100644 --- a/spec/frontend/blob/blob_file_dropzone_spec.js +++ b/spec/frontend/blob/blob_file_dropzone_spec.js @@ -5,10 +5,6 @@ describe('BlobFileDropzone', () => { preloadFixtures('blob/show.html'); let dropzone; let replaceFileButton; - const jQueryMock = { - enable: jest.fn(), - disable: jest.fn(), - }; beforeEach(() => { loadFixtures('blob/show.html'); @@ -18,7 +14,6 @@ describe('BlobFileDropzone', () => { dropzone = $('.js-upload-blob-form .dropzone').get(0).dropzone; dropzone.processQueue = jest.fn(); replaceFileButton = $('#submit-all'); - $.fn.extend(jQueryMock); }); describe('submit button', () => { @@ -43,7 +38,7 @@ describe('BlobFileDropzone', () => { replaceFileButton.click(); expect(window.alert).not.toHaveBeenCalled(); - expect(jQueryMock.enable).toHaveBeenCalled(); + expect(replaceFileButton.is(':disabled')).toEqual(true); expect(dropzone.processQueue).toHaveBeenCalled(); }); }); diff --git a/spec/frontend/boards/board_new_issue_spec.js b/spec/frontend/boards/board_new_issue_spec.js index 4eb7f0c131e..94afc8a2b45 100644 --- a/spec/frontend/boards/board_new_issue_spec.js +++ b/spec/frontend/boards/board_new_issue_spec.js @@ -1,6 +1,5 @@ /* global List */ -import $ from 'jquery'; import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; @@ -15,9 +14,6 @@ describe('Issue boards new issue form', () => { let list; let mock; let newIssueMock; - const jQueryMock = { - enable: jest.fn(), - }; const promiseReturn = { data: { iid: 100, @@ -53,8 +49,6 @@ describe('Issue boards new issue form', () => { }, }).$mount(document.querySelector('.test-container')); - $.fn.extend(jQueryMock); - return Vue.nextTick(); }); @@ -118,7 +112,7 @@ describe('Issue boards new issue form', () => { return Vue.nextTick() .then(submitIssue) .then(() => { - expect(jQueryMock.enable).toHaveBeenCalled(); + expect(vm.$el.querySelector('.btn-success').disabled).toBe(false); }); }); diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js index 8e6a5576015..e0b7e0bc0f3 100644 --- a/spec/frontend/diffs/components/diff_file_header_spec.js +++ b/spec/frontend/diffs/components/diff_file_header_spec.js @@ -61,7 +61,6 @@ describe('DiffFileHeader component', () => { const findTitleLink = () => wrapper.find({ ref: 'titleWrapper' }); const findExpandButton = () => wrapper.find({ ref: 'expandDiffToFullFileButton' }); const findFileActions = () => wrapper.find('.file-actions'); - const findActiveHeader = () => wrapper.find('.is-active'); const findModeChangedLine = () => wrapper.find({ ref: 'fileMode' }); const findLfsLabel = () => wrapper.find('.label-lfs'); const findToggleDiscussionsButton = () => wrapper.find({ ref: 'toggleDiscussionsButton' }); @@ -144,11 +143,6 @@ describe('DiffFileHeader component', () => { expect(wrapper.find(ClipboardButton).exists()).toBe(true); }); - it('contains a active header class if this is the active file header', () => { - createComponent({ isActive: true }); - expect(findActiveHeader().exists()).toBe(true); - }); - describe('for submodule', () => { const submoduleDiffFile = { ...diffFile, diff --git a/spec/frontend/helpers/jquery.js b/spec/frontend/helpers/jquery.js index 6421a592c0c..4af5f904394 100644 --- a/spec/frontend/helpers/jquery.js +++ b/spec/frontend/helpers/jquery.js @@ -1,6 +1,18 @@ import $ from 'jquery'; +// Expose jQuery so specs using jQuery plugins can be imported nicely. +// Here is an issue to explore better alternatives: +// https://gitlab.com/gitlab-org/gitlab/issues/12448 global.$ = $; global.jQuery = $; +// Fail tests for unmocked requests +$.ajax = () => { + const err = new Error( + 'Unexpected unmocked jQuery.ajax() call! Make sure to mock jQuery.ajax() in tests.', + ); + global.fail(err); + throw err; +}; + export default $; diff --git a/spec/frontend/mocks/node/jquery.js b/spec/frontend/mocks/node/jquery.js deleted file mode 100644 index 5c82f65406e..00000000000 --- a/spec/frontend/mocks/node/jquery.js +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable import/no-commonjs */ - -const $ = jest.requireActual('jquery'); - -// Fail tests for unmocked requests -$.ajax = () => { - const err = new Error( - 'Unexpected unmocked jQuery.ajax() call! Make sure to mock jQuery.ajax() in tests.', - ); - global.fail(err); - throw err; -}; - -// jquery is not an ES6 module -module.exports = $; diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js index 2d0cca18647..60e866542a6 100644 --- a/spec/frontend/notes/components/notes_app_spec.js +++ b/spec/frontend/notes/components/notes_app_spec.js @@ -1,4 +1,4 @@ -import $ from 'helpers/jquery'; +import $ from 'jquery'; import AxiosMockAdapter from 'axios-mock-adapter'; import Vue from 'vue'; import { mount } from '@vue/test-utils'; diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js index 88346083f5a..70f7432c65d 100644 --- a/spec/frontend/releases/stores/modules/detail/actions_spec.js +++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js @@ -24,7 +24,14 @@ describe('Release detail actions', () => { let error; beforeEach(() => { - state = createState(); + state = createState({ + projectId: '18', + tagName: 'v1.3', + releasesPagePath: 'path/to/releases/page', + markdownDocsPath: 'path/to/markdown/docs', + markdownPreviewPath: 'path/to/markdown/preview', + updateReleaseApiDocsPath: 'path/to/api/docs', + }); release = cloneDeep(originalRelease); mock = new MockAdapter(axios); gon.api_version = 'v4'; @@ -36,16 +43,6 @@ describe('Release detail actions', () => { mock.restore(); }); - describe('setInitialState', () => { - it(`commits ${types.SET_INITIAL_STATE} with the provided object`, () => { - const initialState = {}; - - return testAction(actions.setInitialState, initialState, state, [ - { type: types.SET_INITIAL_STATE, payload: initialState }, - ]); - }); - }); - describe('requestRelease', () => { it(`commits ${types.REQUEST_RELEASE}`, () => testAction(actions.requestRelease, undefined, state, [{ type: types.REQUEST_RELEASE }])); diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js index 81b2dde75ab..d49c8854ca2 100644 --- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js +++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js @@ -5,115 +5,106 @@ * is resolved */ -import state from '~/releases/stores/modules/detail/state'; +import createState from '~/releases/stores/modules/detail/state'; import mutations from '~/releases/stores/modules/detail/mutations'; import * as types from '~/releases/stores/modules/detail/mutation_types'; import { release } from '../../../mock_data'; describe('Release detail mutations', () => { - let stateClone; + let state; let releaseClone; beforeEach(() => { - stateClone = state(); - releaseClone = JSON.parse(JSON.stringify(release)); - }); - - describe(types.SET_INITIAL_STATE, () => { - it('populates the state with initial values', () => { - const initialState = { - projectId: '18', - tagName: 'v1.3', - releasesPagePath: 'path/to/releases/page', - markdownDocsPath: 'path/to/markdown/docs', - markdownPreviewPath: 'path/to/markdown/preview', - }; - - mutations[types.SET_INITIAL_STATE](stateClone, initialState); - - expect(stateClone).toEqual(expect.objectContaining(initialState)); + state = createState({ + projectId: '18', + tagName: 'v1.3', + releasesPagePath: 'path/to/releases/page', + markdownDocsPath: 'path/to/markdown/docs', + markdownPreviewPath: 'path/to/markdown/preview', + updateReleaseApiDocsPath: 'path/to/api/docs', }); + releaseClone = JSON.parse(JSON.stringify(release)); }); describe(types.REQUEST_RELEASE, () => { it('set state.isFetchingRelease to true', () => { - mutations[types.REQUEST_RELEASE](stateClone); + mutations[types.REQUEST_RELEASE](state); - expect(stateClone.isFetchingRelease).toEqual(true); + expect(state.isFetchingRelease).toEqual(true); }); }); describe(types.RECEIVE_RELEASE_SUCCESS, () => { it('handles a successful response from the server', () => { - mutations[types.RECEIVE_RELEASE_SUCCESS](stateClone, releaseClone); + mutations[types.RECEIVE_RELEASE_SUCCESS](state, releaseClone); - expect(stateClone.fetchError).toEqual(undefined); + expect(state.fetchError).toEqual(undefined); - expect(stateClone.isFetchingRelease).toEqual(false); + expect(state.isFetchingRelease).toEqual(false); - expect(stateClone.release).toEqual(releaseClone); + expect(state.release).toEqual(releaseClone); }); }); describe(types.RECEIVE_RELEASE_ERROR, () => { it('handles an unsuccessful response from the server', () => { const error = { message: 'An error occurred!' }; - mutations[types.RECEIVE_RELEASE_ERROR](stateClone, error); + mutations[types.RECEIVE_RELEASE_ERROR](state, error); - expect(stateClone.isFetchingRelease).toEqual(false); + expect(state.isFetchingRelease).toEqual(false); - expect(stateClone.release).toBeUndefined(); + expect(state.release).toBeUndefined(); - expect(stateClone.fetchError).toEqual(error); + expect(state.fetchError).toEqual(error); }); }); describe(types.UPDATE_RELEASE_TITLE, () => { it("updates the release's title", () => { - stateClone.release = releaseClone; + state.release = releaseClone; const newTitle = 'The new release title'; - mutations[types.UPDATE_RELEASE_TITLE](stateClone, newTitle); + mutations[types.UPDATE_RELEASE_TITLE](state, newTitle); - expect(stateClone.release.name).toEqual(newTitle); + expect(state.release.name).toEqual(newTitle); }); }); describe(types.UPDATE_RELEASE_NOTES, () => { it("updates the release's notes", () => { - stateClone.release = releaseClone; + state.release = releaseClone; const newNotes = 'The new release notes'; - mutations[types.UPDATE_RELEASE_NOTES](stateClone, newNotes); + mutations[types.UPDATE_RELEASE_NOTES](state, newNotes); - expect(stateClone.release.description).toEqual(newNotes); + expect(state.release.description).toEqual(newNotes); }); }); describe(types.REQUEST_UPDATE_RELEASE, () => { it('set state.isUpdatingRelease to true', () => { - mutations[types.REQUEST_UPDATE_RELEASE](stateClone); + mutations[types.REQUEST_UPDATE_RELEASE](state); - expect(stateClone.isUpdatingRelease).toEqual(true); + expect(state.isUpdatingRelease).toEqual(true); }); }); describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => { it('handles a successful response from the server', () => { - mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](stateClone, releaseClone); + mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, releaseClone); - expect(stateClone.updateError).toEqual(undefined); + expect(state.updateError).toEqual(undefined); - expect(stateClone.isUpdatingRelease).toEqual(false); + expect(state.isUpdatingRelease).toEqual(false); }); }); describe(types.RECEIVE_UPDATE_RELEASE_ERROR, () => { it('handles an unsuccessful response from the server', () => { const error = { message: 'An error occurred!' }; - mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](stateClone, error); + mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error); - expect(stateClone.isUpdatingRelease).toEqual(false); + expect(state.isUpdatingRelease).toEqual(false); - expect(stateClone.updateError).toEqual(error); + expect(state.updateError).toEqual(error); }); }); }); diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js index 203781bb6fc..fff76f158dd 100644 --- a/spec/frontend/test_setup.js +++ b/spec/frontend/test_setup.js @@ -1,6 +1,5 @@ import Vue from 'vue'; import * as jqueryMatchers from 'custom-jquery-matchers'; -import $ from 'jquery'; import { config as testUtilsConfig } from '@vue/test-utils'; import Translate from '~/vue_shared/translate'; import { initializeTestTimeout } from './helpers/timeout'; @@ -9,11 +8,9 @@ import { setupManualMocks } from './mocks/mocks_helper'; import customMatchers from './matchers'; import './helpers/dom_shims'; - -// Expose jQuery so specs using jQuery plugins can be imported nicely. -// Here is an issue to explore better alternatives: -// https://gitlab.com/gitlab-org/gitlab/issues/12448 -window.jQuery = $; +import './helpers/jquery'; +import '~/commons/jquery'; +import '~/commons/bootstrap'; process.on('unhandledRejection', global.promiseRejectionHandler); diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js index 75d1ce9cc5b..b3ced84ddb5 100644 --- a/spec/frontend/vue_shared/components/file_row_spec.js +++ b/spec/frontend/vue_shared/components/file_row_spec.js @@ -72,19 +72,6 @@ describe('File row component', () => { }); }); - it('is marked as viewed if clicked', () => { - createComponent({ - file: { - ...file(), - type: 'blob', - fileHash: '#123456789', - }, - level: 0, - viewedFiles: ['#123456789'], - }); - expect(wrapper.classes()).toContain('is-viewed'); - }); - it('indents row based on level', () => { createComponent({ file: file('t4'), diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 1fd6157ce43..9ac2660908c 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -1542,16 +1542,54 @@ describe Gitlab::Database::MigrationHelpers do end describe '#create_or_update_plan_limit' do - it 'creates or updates plan limits' do + class self::Plan < ActiveRecord::Base + self.table_name = 'plans' + end + + class self::PlanLimits < ActiveRecord::Base + self.table_name = 'plan_limits' + end + + it 'properly escapes names' do expect(model).to receive(:execute).with <<~SQL INSERT INTO plan_limits (plan_id, "project_hooks") - VALUES - ((SELECT id FROM plans WHERE name = 'free' LIMIT 1), '10') + SELECT id, '10' FROM plans WHERE name = 'free' LIMIT 1 ON CONFLICT (plan_id) DO UPDATE SET "project_hooks" = EXCLUDED."project_hooks"; SQL model.create_or_update_plan_limit('project_hooks', 'free', 10) end + + context 'when plan does not exist' do + it 'does not create any plan limits' do + expect { model.create_or_update_plan_limit('project_hooks', 'plan_name', 10) } + .not_to change { self.class::PlanLimits.count } + end + end + + context 'when plan does exist' do + let!(:plan) { self.class::Plan.create!(name: 'plan_name') } + + context 'when limit does not exist' do + it 'inserts a new plan limits' do + expect { model.create_or_update_plan_limit('project_hooks', 'plan_name', 10) } + .to change { self.class::PlanLimits.count }.by(1) + + expect(self.class::PlanLimits.pluck(:project_hooks)).to contain_exactly(10) + end + end + + context 'when limit does exist' do + let!(:plan_limit) { self.class::PlanLimits.create!(plan_id: plan.id) } + + it 'updates an existing plan limits' do + expect { model.create_or_update_plan_limit('project_hooks', 'plan_name', 999) } + .not_to change { self.class::PlanLimits.count } + + expect(plan_limit.reload.project_hooks).to eq(999) + end + end + end end describe '#with_lock_retries' do diff --git a/spec/lib/gitlab/x509/commit_spec.rb b/spec/lib/gitlab/x509/commit_spec.rb index 07d7eba6b9a..ac93609b467 100644 --- a/spec/lib/gitlab/x509/commit_spec.rb +++ b/spec/lib/gitlab/x509/commit_spec.rb @@ -5,252 +5,30 @@ describe Gitlab::X509::Commit do describe '#signature' do let(:signature) { described_class.new(commit).signature } - let(:user1_certificate_attributes) do - { - subject_key_identifier: X509Helpers::User1.certificate_subject_key_identifier, - subject: X509Helpers::User1.certificate_subject, - email: X509Helpers::User1.certificate_email, - serial_number: X509Helpers::User1.certificate_serial - } - end - - let(:user1_issuer_attributes) do - { - subject_key_identifier: X509Helpers::User1.issuer_subject_key_identifier, - subject: X509Helpers::User1.certificate_issuer, - crl_url: X509Helpers::User1.certificate_crl - } - end + context 'returns the cached signature' do + let(:commit_sha) { '189a6c924013fc3fe40d6f1ec1dc20214183bc97' } + let(:project) { create(:project, :public, :repository) } + let(:commit) { create(:commit, project: project, sha: commit_sha) } - shared_examples 'returns the cached signature on second call' do - it 'returns the cached signature on second call' do - x509_commit = described_class.new(commit) + it 'on second call' do + allow_any_instance_of(described_class).to receive(:new).and_call_original + expect_any_instance_of(described_class).to receive(:create_cached_signature!).and_call_original - expect(x509_commit).to receive(:create_cached_signature).and_call_original signature # consecutive call - expect(x509_commit).not_to receive(:create_cached_signature).and_call_original + expect(described_class).not_to receive(:create_cached_signature!).and_call_original signature end end - let!(:project) { create :project, :repository, path: X509Helpers::User1.path } - let!(:commit_sha) { X509Helpers::User1.commit } - context 'unsigned commit' do + let!(:project) { create :project, :repository, path: X509Helpers::User1.path } + let!(:commit_sha) { X509Helpers::User1.commit } let!(:commit) { create :commit, project: project, sha: commit_sha } it 'returns nil' do - expect(described_class.new(commit).signature).to be_nil - end - end - - context 'valid signature from known user' do - let!(:commit) { create :commit, project: project, sha: commit_sha, created_at: Time.utc(2019, 1, 1, 20, 15, 0), committer_email: X509Helpers::User1.emails.first } - - let!(:user) { create(:user, email: X509Helpers::User1.emails.first) } - - before do - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return( - [ - X509Helpers::User1.signed_commit_signature, - X509Helpers::User1.signed_commit_base_data - ] - ) - end - - it 'returns an unverified signature' do - expect(signature).to have_attributes( - commit_sha: commit_sha, - project: project, - verification_status: 'unverified' - ) - expect(signature.x509_certificate).to have_attributes(user1_certificate_attributes) - expect(signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes) - expect(signature.persisted?).to be_truthy - end - end - - context 'verified signature from known user' do - let!(:commit) { create :commit, project: project, sha: commit_sha, created_at: Time.utc(2019, 1, 1, 20, 15, 0), committer_email: X509Helpers::User1.emails.first } - - let!(:user) { create(:user, email: X509Helpers::User1.emails.first) } - - before do - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return( - [ - X509Helpers::User1.signed_commit_signature, - X509Helpers::User1.signed_commit_base_data - ] - ) - end - - context 'with trusted certificate store' do - before do - store = OpenSSL::X509::Store.new - certificate = OpenSSL::X509::Certificate.new X509Helpers::User1.trust_cert - store.add_cert(certificate) - allow(OpenSSL::X509::Store).to receive(:new) - .and_return( - store - ) - end - - it 'returns a verified signature' do - expect(signature).to have_attributes( - commit_sha: commit_sha, - project: project, - verification_status: 'verified' - ) - expect(signature.x509_certificate).to have_attributes(user1_certificate_attributes) - expect(signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes) - expect(signature.persisted?).to be_truthy - end - - context 'revoked certificate' do - let(:x509_issuer) { create(:x509_issuer, user1_issuer_attributes) } - let!(:x509_certificate) { create(:x509_certificate, user1_certificate_attributes.merge(x509_issuer_id: x509_issuer.id, certificate_status: :revoked)) } - - it 'returns an unverified signature' do - expect(signature).to have_attributes( - commit_sha: commit_sha, - project: project, - verification_status: 'unverified' - ) - expect(signature.x509_certificate).to have_attributes(user1_certificate_attributes) - expect(signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes) - expect(signature.persisted?).to be_truthy - end - end - end - - context 'without trusted certificate within store' do - before do - store = OpenSSL::X509::Store.new - allow(OpenSSL::X509::Store).to receive(:new) - .and_return( - store - ) - end - - it 'returns an unverified signature' do - expect(signature).to have_attributes( - commit_sha: commit_sha, - project: project, - verification_status: 'unverified' - ) - expect(signature.x509_certificate).to have_attributes(user1_certificate_attributes) - expect(signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes) - expect(signature.persisted?).to be_truthy - end - end - end - - context 'unverified signature from unknown user' do - let!(:commit) { create :commit, project: project, sha: commit_sha, created_at: Time.utc(2019, 1, 1, 20, 15, 0), committer_email: X509Helpers::User1.emails.first } - - before do - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return( - [ - X509Helpers::User1.signed_commit_signature, - X509Helpers::User1.signed_commit_base_data - ] - ) - end - - it 'returns an unverified signature' do - expect(signature).to have_attributes( - commit_sha: commit_sha, - project: project, - verification_status: 'unverified' - ) - expect(signature.x509_certificate).to have_attributes(user1_certificate_attributes) - expect(signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes) - expect(signature.persisted?).to be_truthy - end - end - - context 'invalid signature' do - let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: X509Helpers::User1.emails.first } - - let!(:user) { create(:user, email: X509Helpers::User1.emails.first) } - - before do - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return( - [ - # Corrupt the key - X509Helpers::User1.signed_commit_signature.tr('A', 'B'), - X509Helpers::User1.signed_commit_base_data - ] - ) - end - - it 'returns nil' do - expect(described_class.new(commit).signature).to be_nil - end - end - - context 'invalid commit message' do - let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: X509Helpers::User1.emails.first } - - let!(:user) { create(:user, email: X509Helpers::User1.emails.first) } - - before do - allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) - .with(Gitlab::Git::Repository, commit_sha) - .and_return( - [ - X509Helpers::User1.signed_commit_signature, - # Corrupt the commit message - 'x' - ] - ) - end - - it 'returns nil' do - expect(described_class.new(commit).signature).to be_nil - end - end - - context 'certificate_crl' do - let!(:commit) { create :commit, project: project, sha: commit_sha, created_at: Time.utc(2019, 1, 1, 20, 15, 0), committer_email: X509Helpers::User1.emails.first } - let(:signed_commit) { described_class.new(commit) } - - describe 'valid crlDistributionPoints' do - before do - allow(signed_commit).to receive(:get_certificate_extension).and_call_original - - allow(signed_commit).to receive(:get_certificate_extension) - .with('crlDistributionPoints') - .and_return("\nFull Name:\n URI:http://ch.siemens.com/pki?ZZZZZZA2.crl\n URI:ldap://cl.siemens.net/CN=ZZZZZZA2,L=PKI?certificateRevocationList\n URI:ldap://cl.siemens.com/CN=ZZZZZZA2,o=Trustcenter?certificateRevocationList\n") - end - - it 'returns an unverified signature' do - expect(signed_commit.signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes) - end - end - - describe 'valid crlDistributionPoints providing multiple http URIs' do - before do - allow(signed_commit).to receive(:get_certificate_extension).and_call_original - - allow(signed_commit).to receive(:get_certificate_extension) - .with('crlDistributionPoints') - .and_return("\nFull Name:\n URI:http://cdp1.pca.dfn.de/dfn-ca-global-g2/pub/crl/cacrl.crl\n\nFull Name:\n URI:http://cdp2.pca.dfn.de/dfn-ca-global-g2/pub/crl/cacrl.crl\n") - end - - it 'extracts the first URI' do - expect(signed_commit.signature.x509_certificate.x509_issuer.crl_url).to eq("http://cdp1.pca.dfn.de/dfn-ca-global-g2/pub/crl/cacrl.crl") - end + expect(signature).to be_nil end end end diff --git a/spec/lib/gitlab/x509/signature_spec.rb b/spec/lib/gitlab/x509/signature_spec.rb new file mode 100644 index 00000000000..6c585acd5cd --- /dev/null +++ b/spec/lib/gitlab/x509/signature_spec.rb @@ -0,0 +1,232 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::X509::Signature do + let(:issuer_attributes) do + { + subject_key_identifier: X509Helpers::User1.issuer_subject_key_identifier, + subject: X509Helpers::User1.certificate_issuer, + crl_url: X509Helpers::User1.certificate_crl + } + end + + context 'commit signature' do + let(:certificate_attributes) do + { + subject_key_identifier: X509Helpers::User1.certificate_subject_key_identifier, + subject: X509Helpers::User1.certificate_subject, + email: X509Helpers::User1.certificate_email, + serial_number: X509Helpers::User1.certificate_serial + } + end + + context 'verified signature' do + context 'with trusted certificate store' do + before do + store = OpenSSL::X509::Store.new + certificate = OpenSSL::X509::Certificate.new(X509Helpers::User1.trust_cert) + store.add_cert(certificate) + allow(OpenSSL::X509::Store).to receive(:new).and_return(store) + end + + it 'returns a verified signature if email does match' do + signature = described_class.new( + X509Helpers::User1.signed_commit_signature, + X509Helpers::User1.signed_commit_base_data, + X509Helpers::User1.certificate_email, + X509Helpers::User1.signed_commit_time + ) + + expect(signature.x509_certificate).to have_attributes(certificate_attributes) + expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) + expect(signature.verified_signature).to be_truthy + expect(signature.verification_status).to eq(:verified) + end + + it 'returns an unverified signature if email does not match' do + signature = described_class.new( + X509Helpers::User1.signed_commit_signature, + X509Helpers::User1.signed_commit_base_data, + "gitlab@example.com", + X509Helpers::User1.signed_commit_time + ) + + expect(signature.x509_certificate).to have_attributes(certificate_attributes) + expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) + expect(signature.verified_signature).to be_truthy + expect(signature.verification_status).to eq(:unverified) + end + + it 'returns an unverified signature if email does match and time is wrong' do + signature = described_class.new( + X509Helpers::User1.signed_commit_signature, + X509Helpers::User1.signed_commit_base_data, + X509Helpers::User1.certificate_email, + Time.new(2020, 2, 22) + ) + + expect(signature.x509_certificate).to have_attributes(certificate_attributes) + expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) + expect(signature.verified_signature).to be_falsey + expect(signature.verification_status).to eq(:unverified) + end + + it 'returns an unverified signature if certificate is revoked' do + signature = described_class.new( + X509Helpers::User1.signed_commit_signature, + X509Helpers::User1.signed_commit_base_data, + X509Helpers::User1.certificate_email, + X509Helpers::User1.signed_commit_time + ) + + expect(signature.verification_status).to eq(:verified) + + signature.x509_certificate.revoked! + + expect(signature.verification_status).to eq(:unverified) + end + end + + context 'without trusted certificate within store' do + before do + store = OpenSSL::X509::Store.new + allow(OpenSSL::X509::Store).to receive(:new) + .and_return( + store + ) + end + + it 'returns an unverified signature' do + signature = described_class.new( + X509Helpers::User1.signed_commit_signature, + X509Helpers::User1.signed_commit_base_data, + X509Helpers::User1.certificate_email, + X509Helpers::User1.signed_commit_time + ) + + expect(signature.x509_certificate).to have_attributes(certificate_attributes) + expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) + expect(signature.verified_signature).to be_falsey + expect(signature.verification_status).to eq(:unverified) + end + end + end + + context 'invalid signature' do + it 'returns nil' do + signature = described_class.new( + X509Helpers::User1.signed_commit_signature.tr('A', 'B'), + X509Helpers::User1.signed_commit_base_data, + X509Helpers::User1.certificate_email, + X509Helpers::User1.signed_commit_time + ) + expect(signature.x509_certificate).to be_nil + expect(signature.verified_signature).to be_falsey + expect(signature.verification_status).to eq(:unverified) + end + end + + context 'invalid commit message' do + it 'returns nil' do + signature = described_class.new( + X509Helpers::User1.signed_commit_signature, + 'x', + X509Helpers::User1.certificate_email, + X509Helpers::User1.signed_commit_time + ) + expect(signature.x509_certificate).to be_nil + expect(signature.verified_signature).to be_falsey + expect(signature.verification_status).to eq(:unverified) + end + end + end + + context 'certificate_crl' do + describe 'valid crlDistributionPoints' do + before do + allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension).and_call_original + + allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension) + .with('crlDistributionPoints') + .and_return("\nFull Name:\n URI:http://ch.siemens.com/pki?ZZZZZZA2.crl\n URI:ldap://cl.siemens.net/CN=ZZZZZZA2,L=PKI?certificateRevocationList\n URI:ldap://cl.siemens.com/CN=ZZZZZZA2,o=Trustcenter?certificateRevocationList\n") + end + + it 'creates an issuer' do + signature = described_class.new( + X509Helpers::User1.signed_commit_signature, + X509Helpers::User1.signed_commit_base_data, + X509Helpers::User1.certificate_email, + X509Helpers::User1.signed_commit_time + ) + + expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) + end + end + + describe 'valid crlDistributionPoints providing multiple http URIs' do + before do + allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension).and_call_original + + allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension) + .with('crlDistributionPoints') + .and_return("\nFull Name:\n URI:http://cdp1.pca.dfn.de/dfn-ca-global-g2/pub/crl/cacrl.crl\n\nFull Name:\n URI:http://cdp2.pca.dfn.de/dfn-ca-global-g2/pub/crl/cacrl.crl\n") + end + + it 'extracts the first URI' do + signature = described_class.new( + X509Helpers::User1.signed_commit_signature, + X509Helpers::User1.signed_commit_base_data, + X509Helpers::User1.certificate_email, + X509Helpers::User1.signed_commit_time + ) + + expect(signature.x509_certificate.x509_issuer.crl_url).to eq("http://cdp1.pca.dfn.de/dfn-ca-global-g2/pub/crl/cacrl.crl") + end + end + end + + context 'email' do + describe 'subjectAltName with email, othername' do + before do + allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension).and_call_original + + allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension) + .with('subjectAltName') + .and_return("email:gitlab@example.com, othername:") + end + + it 'extracts email' do + signature = described_class.new( + X509Helpers::User1.signed_commit_signature, + X509Helpers::User1.signed_commit_base_data, + 'gitlab@example.com', + X509Helpers::User1.signed_commit_time + ) + + expect(signature.x509_certificate.email).to eq("gitlab@example.com") + end + end + + describe 'subjectAltName with othername, email' do + before do + allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension).and_call_original + + allow_any_instance_of(Gitlab::X509::Signature).to receive(:get_certificate_extension) + .with('subjectAltName') + .and_return("othername:, email:gitlab@example.com") + end + + it 'extracts email' do + signature = described_class.new( + X509Helpers::User1.signed_commit_signature, + X509Helpers::User1.signed_commit_base_data, + 'gitlab@example.com', + X509Helpers::User1.signed_commit_time + ) + + expect(signature.x509_certificate.email).to eq("gitlab@example.com") + end + end + end +end diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb index 05abdf76be9..14b292db045 100644 --- a/spec/requests/api/pipeline_schedules_spec.rb +++ b/spec/requests/api/pipeline_schedules_spec.rb @@ -46,7 +46,7 @@ describe API::PipelineSchedules do get api("/projects/#{project.id}/pipeline_schedules", developer) end.count - create_pipeline_schedules(10) + create_pipeline_schedules(5) expect do get api("/projects/#{project.id}/pipeline_schedules", developer) diff --git a/spec/support/helpers/x509_helpers.rb b/spec/support/helpers/x509_helpers.rb index f72b518134c..9ea997bf5f4 100644 --- a/spec/support/helpers/x509_helpers.rb +++ b/spec/support/helpers/x509_helpers.rb @@ -169,6 +169,10 @@ module X509Helpers SIGNEDDATA end + def signed_commit_time + Time.at(1561027326) + end + def certificate_crl 'http://ch.siemens.com/pki?ZZZZZZA2.crl' end -- cgit v1.2.3