diff options
Diffstat (limited to 'spec')
33 files changed, 638 insertions, 185 deletions
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 975dc035f2d..3cec59050ab 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -6,7 +6,7 @@ describe 'Dropdown author', js: true do let!(:project) { create(:project) } let!(:user) { create(:user, name: 'administrator', username: 'root') } let!(:user_john) { create(:user, name: 'John', username: 'th0mas') } - let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') } + let!(:user_jacob) { create(:user, name: 'Jacob', username: 'ooter32') } let(:filtered_search) { find('.filtered-search') } let(:js_dropdown_author) { '#js-dropdown-author' } @@ -82,31 +82,31 @@ describe 'Dropdown author', js: true do end it 'filters by name' do - send_keys_to_filtered_search('ja') + send_keys_to_filtered_search('jac') expect(dropdown_author_size).to eq(1) end it 'filters by case insensitive name' do - send_keys_to_filtered_search('Ja') + send_keys_to_filtered_search('Jac') expect(dropdown_author_size).to eq(1) end it 'filters by username with symbol' do - send_keys_to_filtered_search('@ot') + send_keys_to_filtered_search('@oot') expect(dropdown_author_size).to eq(2) end it 'filters by username without symbol' do - send_keys_to_filtered_search('ot') + send_keys_to_filtered_search('oot') expect(dropdown_author_size).to eq(2) end it 'filters by case insensitive username without symbol' do - send_keys_to_filtered_search('OT') + send_keys_to_filtered_search('OOT') expect(dropdown_author_size).to eq(2) end diff --git a/spec/features/projects/commit/rss_spec.rb b/spec/features/projects/commits/rss_spec.rb index db958346f06..db958346f06 100644 --- a/spec/features/projects/commit/rss_spec.rb +++ b/spec/features/projects/commits/rss_spec.rb diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb new file mode 100644 index 00000000000..41f3c15a94c --- /dev/null +++ b/spec/features/projects/commits/user_browses_commits_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'User broweses commits' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository, namespace: user.namespace) } + + before do + project.add_master(user) + sign_in(user) + end + + context 'primary email' do + it 'finds a commit by a primary email' do + user = create(:user, email: 'dmitriy.zaporozhets@gmail.com') + + visit(project_commit_path(project, RepoHelpers.sample_commit.id)) + + check_author_link(RepoHelpers.sample_commit.author_email, user) + end + end + + context 'secondary email' do + it 'finds a commit by a secondary email' do + user = + create(:user) do |user| + create(:email, { user: user, email: 'dmitriy.zaporozhets@gmail.com' }) + end + + visit(project_commit_path(project, RepoHelpers.sample_commit.parent_id)) + + check_author_link(RepoHelpers.sample_commit.author_email, user) + end + end +end + +private + +def check_author_link(email, author) + author_link = find('.commit-author-link') + + expect(author_link['href']).to eq(user_path(author)) + expect(author_link['title']).to eq(email) + expect(find('.commit-author-name').text).to eq(author.name) +end diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json index bd6bfc03199..8acd9488215 100644 --- a/spec/fixtures/api/schemas/public_api/v4/issues.json +++ b/spec/fixtures/api/schemas/public_api/v4/issues.json @@ -78,7 +78,13 @@ "downvotes": { "type": "integer" }, "due_date": { "type": ["date", "null"] }, "confidential": { "type": "boolean" }, - "web_url": { "type": "uri" } + "web_url": { "type": "uri" }, + "time_stats": { + "time_estimate": { "type": "integer" }, + "total_time_spent": { "type": "integer" }, + "human_time_estimate": { "type": ["string", "null"] }, + "human_total_time_spent": { "type": ["string", "null"] } + } }, "required": [ "id", "iid", "project_id", "title", "description", diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json index 60aa47c1259..31b3f4ba946 100644 --- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json +++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json @@ -72,7 +72,13 @@ "user_notes_count": { "type": "integer" }, "should_remove_source_branch": { "type": ["boolean", "null"] }, "force_remove_source_branch": { "type": ["boolean", "null"] }, - "web_url": { "type": "uri" } + "web_url": { "type": "uri" }, + "time_stats": { + "time_estimate": { "type": "integer" }, + "total_time_spent": { "type": "integer" }, + "human_time_estimate": { "type": ["string", "null"] }, + "human_total_time_spent": { "type": ["string", "null"] } + } }, "required": [ "id", "iid", "project_id", "title", "description", diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index 2e81a1b056b..0847e463577 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -1,4 +1,3 @@ -import Cookies from 'js-cookie'; import { calculateTop, showSubLevelItems, @@ -11,6 +10,7 @@ import { getHideSubItemsInterval, documentMouseMove, getHeaderHeight, + setSidebar, } from '~/fly_out_nav'; import bp from '~/breakpoints'; @@ -113,7 +113,6 @@ describe('Fly out sidebar navigation', () => { clientX: el.getBoundingClientRect().left + 20, clientY: el.getBoundingClientRect().top + 10, }); - console.log(el); expect( getHideSubItemsInterval(), @@ -283,7 +282,7 @@ describe('Fly out sidebar navigation', () => { describe('canShowActiveSubItems', () => { afterEach(() => { - Cookies.remove('sidebar_collapsed'); + setSidebar(null); }); it('returns true by default', () => { @@ -292,36 +291,23 @@ describe('Fly out sidebar navigation', () => { ).toBeTruthy(); }); - it('returns false when cookie is false & element is active', () => { - Cookies.set('sidebar_collapsed', 'false'); + it('returns false when active & expanded sidebar', () => { + const sidebar = document.createElement('div'); el.classList.add('active'); - expect( - canShowActiveSubItems(el), - ).toBeFalsy(); - }); - - it('returns true when cookie is false & element is active', () => { - Cookies.set('sidebar_collapsed', 'true'); - el.classList.add('active'); + setSidebar(sidebar); expect( canShowActiveSubItems(el), - ).toBeTruthy(); + ).toBeFalsy(); }); - it('returns true when element is active & breakpoint is sm', () => { - breakpointSize = 'sm'; + it('returns true when active & collapsed sidebar', () => { + const sidebar = document.createElement('div'); + sidebar.classList.add('sidebar-icons-only'); el.classList.add('active'); - expect( - canShowActiveSubItems(el), - ).toBeTruthy(); - }); - - it('returns true when element is active & breakpoint is md', () => { - breakpointSize = 'md'; - el.classList.add('active'); + setSidebar(sidebar); expect( canShowActiveSubItems(el), diff --git a/spec/javascripts/issue_show/components/edited_spec.js b/spec/javascripts/issue_show/components/edited_spec.js index a0d0750ae34..2061def699b 100644 --- a/spec/javascripts/issue_show/components/edited_spec.js +++ b/spec/javascripts/issue_show/components/edited_spec.js @@ -46,4 +46,14 @@ describe('edited', () => { expect(editedComponent.$el.querySelector('.author_link')).toBeFalsy(); expect(editedComponent.$el.querySelector('time')).toBeTruthy(); }); + + it('renders time ago tooltip at the bottom', () => { + const editedComponent = new EditedComponent({ + propsData: { + updatedAt: '2017-05-15T12:31:04.428Z', + }, + }).$mount(); + + expect(editedComponent.$el.querySelector('time').dataset.placement).toEqual('bottom'); + }); }); diff --git a/spec/javascripts/monitoring/monitoring_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 6c7b691baa4..752fdfb4614 100644 --- a/spec/javascripts/monitoring/monitoring_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -1,21 +1,21 @@ import Vue from 'vue'; -import Monitoring from '~/monitoring/components/monitoring.vue'; +import Dashboard from '~/monitoring/components/dashboard.vue'; import { MonitorMockInterceptor } from './mock_data'; -describe('Monitoring', () => { +describe('Dashboard', () => { const fixtureName = 'environments/metrics/metrics.html.raw'; - let MonitoringComponent; + let DashboardComponent; let component; preloadFixtures(fixtureName); beforeEach(() => { loadFixtures(fixtureName); - MonitoringComponent = Vue.extend(Monitoring); + DashboardComponent = Vue.extend(Dashboard); }); describe('no metrics are available yet', () => { it('shows a getting started empty state when no metrics are present', () => { - component = new MonitoringComponent({ + component = new DashboardComponent({ el: document.querySelector('#prometheus-graphs'), }); @@ -36,7 +36,7 @@ describe('Monitoring', () => { }); it('shows up a loading state', (done) => { - component = new MonitoringComponent({ + component = new DashboardComponent({ el: document.querySelector('#prometheus-graphs'), }); component.$mount(); diff --git a/spec/javascripts/monitoring/monitoring_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js index 4c0c558502f..e8f7042e131 100644 --- a/spec/javascripts/monitoring/monitoring_state_spec.js +++ b/spec/javascripts/monitoring/dashboard_state_spec.js @@ -1,9 +1,9 @@ import Vue from 'vue'; -import MonitoringState from '~/monitoring/components/monitoring_state.vue'; +import EmptyState from '~/monitoring/components/empty_state.vue'; import { statePaths } from './mock_data'; const createComponent = (propsData) => { - const Component = Vue.extend(MonitoringState); + const Component = Vue.extend(EmptyState); return new Component({ propsData, @@ -14,7 +14,7 @@ function getTextFromNode(component, selector) { return component.$el.querySelector(selector).firstChild.nodeValue.trim(); } -describe('MonitoringState', () => { +describe('EmptyState', () => { describe('Computed props', () => { it('currentState', () => { const component = createComponent({ diff --git a/spec/javascripts/monitoring/monitoring_deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js index 5cc5b514824..c2ff38ffab9 100644 --- a/spec/javascripts/monitoring/monitoring_deployment_spec.js +++ b/spec/javascripts/monitoring/graph/deployment_spec.js @@ -1,9 +1,9 @@ import Vue from 'vue'; -import MonitoringState from '~/monitoring/components/monitoring_deployment.vue'; -import { deploymentData } from './mock_data'; +import GraphDeployment from '~/monitoring/components/graph/deployment.vue'; +import { deploymentData } from '../mock_data'; const createComponent = (propsData) => { - const Component = Vue.extend(MonitoringState); + const Component = Vue.extend(GraphDeployment); return new Component({ propsData, diff --git a/spec/javascripts/monitoring/monitoring_flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js index 3861a95ff07..731076a7d2a 100644 --- a/spec/javascripts/monitoring/monitoring_flag_spec.js +++ b/spec/javascripts/monitoring/graph/flag_spec.js @@ -1,8 +1,8 @@ import Vue from 'vue'; -import MonitoringFlag from '~/monitoring/components/monitoring_flag.vue'; +import GraphFlag from '~/monitoring/components/graph/flag.vue'; const createComponent = (propsData) => { - const Component = Vue.extend(MonitoringFlag); + const Component = Vue.extend(GraphFlag); return new Component({ propsData, @@ -14,7 +14,7 @@ function getCoordinate(component, selector, coordinate) { return parseInt(coordinateVal, 10); } -describe('MonitoringFlag', () => { +describe('GraphFlag', () => { it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => { const component = createComponent({ currentXCoordinate: 200, diff --git a/spec/javascripts/monitoring/monitoring_legends_spec.js b/spec/javascripts/monitoring/graph/legend_spec.js index 4c69b81e650..e877832dffd 100644 --- a/spec/javascripts/monitoring/monitoring_legends_spec.js +++ b/spec/javascripts/monitoring/graph/legend_spec.js @@ -1,9 +1,9 @@ import Vue from 'vue'; -import MonitoringLegends from '~/monitoring/components/monitoring_legends.vue'; +import GraphLegend from '~/monitoring/components/graph/legend.vue'; import measurements from '~/monitoring/utils/measurements'; const createComponent = (propsData) => { - const Component = Vue.extend(MonitoringLegends); + const Component = Vue.extend(GraphLegend); return new Component({ propsData, @@ -14,7 +14,7 @@ function getTextFromNode(component, selector) { return component.$el.querySelector(selector).firstChild.nodeValue.trim(); } -describe('MonitoringLegends', () => { +describe('GraphLegend', () => { describe('Computed props', () => { it('textTransform', () => { const component = createComponent({ diff --git a/spec/javascripts/monitoring/monitoring_row_spec.js b/spec/javascripts/monitoring/graph_row_spec.js index a82480e8342..dd485473ccf 100644 --- a/spec/javascripts/monitoring/monitoring_row_spec.js +++ b/spec/javascripts/monitoring/graph_row_spec.js @@ -1,16 +1,21 @@ import Vue from 'vue'; -import MonitoringRow from '~/monitoring/components/monitoring_row.vue'; +import GraphRow from '~/monitoring/components/graph_row.vue'; +import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; import { deploymentData, singleRowMetrics } from './mock_data'; const createComponent = (propsData) => { - const Component = Vue.extend(MonitoringRow); + const Component = Vue.extend(GraphRow); return new Component({ propsData, }).$mount(); }; -describe('MonitoringRow', () => { +describe('GraphRow', () => { + beforeEach(() => { + spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({}); + }); + describe('Computed props', () => { it('bootstrapClass is set to col-md-6 when rowData is higher/equal to 2', () => { const component = createComponent({ diff --git a/spec/javascripts/monitoring/monitoring_column_spec.js b/spec/javascripts/monitoring/graph_spec.js index c423024dce0..6d6fe410113 100644 --- a/spec/javascripts/monitoring/monitoring_column_spec.js +++ b/spec/javascripts/monitoring/graph_spec.js @@ -1,39 +1,37 @@ import Vue from 'vue'; import _ from 'underscore'; -import MonitoringColumn from '~/monitoring/components/monitoring_column.vue'; +import Graph from '~/monitoring/components/graph.vue'; import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; import eventHub from '~/monitoring/event_hub'; import { deploymentData, singleRowMetrics } from './mock_data'; const createComponent = (propsData) => { - const Component = Vue.extend(MonitoringColumn); + const Component = Vue.extend(Graph); return new Component({ propsData, }).$mount(); }; -describe('MonitoringColumn', () => { +describe('Graph', () => { beforeEach(() => { - spyOn(MonitoringMixins.methods, 'formatDeployments').and.callFake(function fakeFormat() { - return {}; - }); + spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({}); }); it('has a title', () => { const component = createComponent({ - columnData: singleRowMetrics[0], + graphData: singleRowMetrics[0], classType: 'col-md-6', updateAspectRatio: false, deploymentData, }); - expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.columnData.title); + expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title); }); it('creates a path for the line and area of the graph', (done) => { const component = createComponent({ - columnData: singleRowMetrics[0], + graphData: singleRowMetrics[0], classType: 'col-md-6', updateAspectRatio: false, deploymentData, @@ -53,7 +51,7 @@ describe('MonitoringColumn', () => { describe('Computed props', () => { it('axisTransform translates an element Y position depending of its height', () => { const component = createComponent({ - columnData: singleRowMetrics[0], + graphData: singleRowMetrics[0], classType: 'col-md-6', updateAspectRatio: false, deploymentData, @@ -66,7 +64,7 @@ describe('MonitoringColumn', () => { it('outterViewBox gets a width and height property based on the DOM size of the element', () => { const component = createComponent({ - columnData: singleRowMetrics[0], + graphData: singleRowMetrics[0], classType: 'col-md-6', updateAspectRatio: false, deploymentData, @@ -81,7 +79,7 @@ describe('MonitoringColumn', () => { it('sends an event to the eventhub when it has finished resizing', (done) => { const component = createComponent({ - columnData: singleRowMetrics[0], + graphData: singleRowMetrics[0], classType: 'col-md-6', updateAspectRatio: false, deploymentData, @@ -97,13 +95,13 @@ describe('MonitoringColumn', () => { it('has a title for the y-axis and the chart legend that comes from the backend', () => { const component = createComponent({ - columnData: singleRowMetrics[0], + graphData: singleRowMetrics[0], classType: 'col-md-6', updateAspectRatio: false, deploymentData, }); - expect(component.yAxisLabel).toEqual(component.columnData.y_label); - expect(component.legendTitle).toEqual(component.columnData.queries[0].label); + expect(component.yAxisLabel).toEqual(component.graphData.y_label); + expect(component.legendTitle).toEqual(component.graphData.queries[0].label); }); }); diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index ee28387cd48..c70a4cb55fe 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -344,28 +344,31 @@ module Ci let(:config) { { rspec: { script: "rspec", type: "test", only: only } } } let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - shared_examples 'raises an error' do - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:only config should be an array of strings or regexps') - end - end - context 'when it is integer' do let(:only) { 1 } - it_behaves_like 'raises an error' + it do + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, + 'jobs:rspec:only has to be either an array of conditions or a hash') + end end context 'when it is an array of integers' do let(:only) { [1, 1] } - it_behaves_like 'raises an error' + it do + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, + 'jobs:rspec:only config should be an array of strings or regexps') + end end context 'when it is invalid regex' do let(:only) { ["/*invalid/"] } - it_behaves_like 'raises an error' + it do + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, + 'jobs:rspec:only config should be an array of strings or regexps') + end end end end @@ -518,28 +521,31 @@ module Ci let(:config) { { rspec: { script: "rspec", except: except } } } let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - shared_examples 'raises an error' do - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:except config should be an array of strings or regexps') - end - end - context 'when it is integer' do let(:except) { 1 } - it_behaves_like 'raises an error' + it do + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, + 'jobs:rspec:except has to be either an array of conditions or a hash') + end end context 'when it is an array of integers' do let(:except) { [1, 1] } - it_behaves_like 'raises an error' + it do + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, + 'jobs:rspec:except config should be an array of strings or regexps') + end end context 'when it is invalid regex' do let(:except) { ["/*invalid/"] } - it_behaves_like 'raises an error' + it do + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, + 'jobs:rspec:except config should be an array of strings or regexps') + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/attributable_spec.rb b/spec/lib/gitlab/ci/config/entry/attributable_spec.rb index fde03c51e2c..b028b771375 100644 --- a/spec/lib/gitlab/ci/config/entry/attributable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/attributable_spec.rb @@ -1,18 +1,21 @@ require 'spec_helper' describe Gitlab::Ci::Config::Entry::Attributable do - let(:node) { Class.new } + let(:node) do + Class.new do + include Gitlab::Ci::Config::Entry::Attributable + end + end + let(:instance) { node.new } before do - node.include(described_class) - node.class_eval do attributes :name, :test end end - context 'config is a hash' do + context 'when config is a hash' do before do allow(instance) .to receive(:config) @@ -29,7 +32,7 @@ describe Gitlab::Ci::Config::Entry::Attributable do end end - context 'config is not a hash' do + context 'when config is not a hash' do before do allow(instance) .to receive(:config) @@ -40,4 +43,18 @@ describe Gitlab::Ci::Config::Entry::Attributable do expect(instance.test).to be_nil end end + + context 'when method is already defined in a superclass' do + it 'raises an error' do + expectation = expect do + Class.new(String) do + include Gitlab::Ci::Config::Entry::Attributable + + attributes :length + end + end + + expectation.to raise_error(ArgumentError, 'Method already defined!') + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb b/spec/lib/gitlab/ci/config/entry/configurable_spec.rb index ae7e628b5b5..088d4b472da 100644 --- a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/configurable_spec.rb @@ -1,40 +1,26 @@ require 'spec_helper' describe Gitlab::Ci::Config::Entry::Configurable do - let(:entry) { Class.new } - - before do - entry.include(described_class) + let(:entry) do + Class.new(Gitlab::Ci::Config::Entry::Node) do + include Gitlab::Ci::Config::Entry::Configurable + end end describe 'validations' do - let(:validator) { entry.validator.new(instance) } - - before do - entry.class_eval do - attr_reader :config + context 'when entry is a hash' do + let(:instance) { entry.new(key: 'value') } - def initialize(config) - @config = config - end + it 'correctly validates an instance' do + expect(instance).to be_valid end - - validator.validate end - context 'when entry validator is invalid' do + context 'when entry is not a hash' do let(:instance) { entry.new('ls') } - it 'returns invalid validator' do - expect(validator).to be_invalid - end - end - - context 'when entry instance is valid' do - let(:instance) { entry.new(key: 'value') } - - it 'returns valid validator' do - expect(validator).to be_valid + it 'invalidates the instance' do + expect(instance).not_to be_valid end end end diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb new file mode 100644 index 00000000000..36a84da4a52 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Policy do + let(:entry) { described_class.new(config) } + + context 'when using simplified policy' do + describe 'validations' do + context 'when entry config value is valid' do + context 'when config is a branch or tag name' do + let(:config) { %w[master feature/branch] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq config + end + end + end + + context 'when config is a regexp' do + let(:config) { ['/^issue-.*$/'] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when config is a special keyword' do + let(:config) { %w[tags triggers branches] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + end + + context 'when entry value is not valid' do + let(:config) { [1] } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include /policy config should be an array of strings or regexps/ + end + end + end + end + end + + context 'when policy strategy does not match' do + let(:config) { 'string strategy' } + + it 'returns information about errors' do + expect(entry.errors) + .to include /has to be either an array of conditions or a hash/ + end + end + + describe '.default' do + it 'does not have a default value' do + expect(described_class.default).to be_nil + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb b/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb new file mode 100644 index 00000000000..395062207a3 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Simplifiable do + describe '.strategy' do + let(:entry) do + Class.new(described_class) do + strategy :Something, if: -> { 'condition' } + strategy :DifferentOne, if: -> { 'condition' } + end + end + + it 'defines entry strategies' do + expect(entry.strategies.size).to eq 2 + expect(entry.strategies.map(&:name)) + .to eq %i[Something DifferentOne] + end + end + + describe 'setting strategy by a condition' do + let(:first) { double('first strategy') } + let(:second) { double('second strategy') } + let(:unknown) { double('unknown strategy') } + + before do + stub_const("#{described_class.name}::Something", first) + stub_const("#{described_class.name}::DifferentOne", second) + stub_const("#{described_class.name}::UnknownStrategy", unknown) + end + + context 'when first strategy should be used' do + let(:entry) do + Class.new(described_class) do + strategy :Something, if: -> (arg) { arg == 'something' } + strategy :DifferentOne, if: -> (*) { false } + end + end + + it 'attemps to load a first strategy' do + expect(first).to receive(:new).with('something', anything) + + entry.new('something') + end + end + + context 'when second strategy should be used' do + let(:entry) do + Class.new(described_class) do + strategy :Something, if: -> (arg) { arg == 'something' } + strategy :DifferentOne, if: -> (arg) { arg == 'test' } + end + end + + it 'attemps to load a second strategy' do + expect(second).to receive(:new).with('test', anything) + + entry.new('test') + end + end + + context 'when neither one is a valid strategy' do + let(:entry) do + Class.new(described_class) do + strategy :Something, if: -> (*) { false } + strategy :DifferentOne, if: -> (*) { false } + end + end + + it 'instantiates an unknown strategy' do + expect(unknown).to receive(:new).with('test', anything) + + entry.new('test') + end + end + end + + context 'when a unknown strategy class is not defined' do + let(:entry) do + Class.new(described_class) do + strategy :String, if: -> (*) { true } + end + end + + it 'raises an error when being initialized' do + expect { entry.new('something') } + .to raise_error ArgumentError, /UndefinedStrategy not available!/ + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb deleted file mode 100644 index e4ee44f1274..00000000000 --- a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Ci::Config::Entry::Trigger do - let(:entry) { described_class.new(config) } - - describe 'validations' do - context 'when entry config value is valid' do - context 'when config is a branch or tag name' do - let(:config) { %w[master feature/branch] } - - describe '#valid?' do - it 'is valid' do - expect(entry).to be_valid - end - end - - describe '#value' do - it 'returns key value' do - expect(entry.value).to eq config - end - end - end - - context 'when config is a regexp' do - let(:config) { ['/^issue-.*$/'] } - - describe '#valid?' do - it 'is valid' do - expect(entry).to be_valid - end - end - end - - context 'when config is a special keyword' do - let(:config) { %w[tags triggers branches] } - - describe '#valid?' do - it 'is valid' do - expect(entry).to be_valid - end - end - end - end - - context 'when entry value is not valid' do - let(:config) { [1] } - - describe '#errors' do - it 'saves errors' do - expect(entry.errors) - .to include 'trigger config should be an array of strings or regexps' - end - end - end - end -end diff --git a/spec/lib/gitlab/ci/config/entry/validatable_spec.rb b/spec/lib/gitlab/ci/config/entry/validatable_spec.rb index d1856801827..ae2a7a51ba6 100644 --- a/spec/lib/gitlab/ci/config/entry/validatable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/validatable_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' describe Gitlab::Ci::Config::Entry::Validatable do - let(:entry) { Class.new } - - before do - entry.include(described_class) + let(:entry) do + Class.new(Gitlab::Ci::Config::Entry::Node) do + include Gitlab::Ci::Config::Entry::Validatable + end end describe '.validator' do @@ -28,7 +28,7 @@ describe Gitlab::Ci::Config::Entry::Validatable do end context 'when validating entry instance' do - let(:entry_instance) { entry.new } + let(:entry_instance) { entry.new('something') } context 'when attribute is valid' do before do diff --git a/spec/lib/gitlab/ci/config/entry/validator_spec.rb b/spec/lib/gitlab/ci/config/entry/validator_spec.rb index ad7e6f07d3c..172b6b47a4f 100644 --- a/spec/lib/gitlab/ci/config/entry/validator_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/validator_spec.rb @@ -48,7 +48,7 @@ describe Gitlab::Ci::Config::Entry::Validator do validator_instance.validate expect(validator_instance.messages) - .to include "node test attribute can't be blank" + .to include /test attribute can't be blank/ end end end diff --git a/spec/lib/gitlab/database/grant_spec.rb b/spec/lib/gitlab/database/grant_spec.rb new file mode 100644 index 00000000000..651da3e8476 --- /dev/null +++ b/spec/lib/gitlab/database/grant_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Gitlab::Database::Grant do + describe '.scope_to_current_user' do + it 'scopes the relation to the current user' do + user = Gitlab::Database.username + column = Gitlab::Database.postgresql? ? :grantee : :User + names = described_class.scope_to_current_user.pluck(column).uniq + + expect(names).to eq([user]) + end + end + + describe '.create_and_execute_trigger' do + it 'returns true when the user can create and execute a trigger' do + # We assume the DB/user is set up correctly so that triggers can be + # created, which is necessary anyway for other tests to work. + expect(described_class.create_and_execute_trigger?('users')).to eq(true) + end + + it 'returns false when the user can not create and/or execute a trigger' do + allow(described_class).to receive(:scope_to_current_user) + .and_return(described_class.none) + + result = described_class.create_and_execute_trigger?('kittens') + + expect(result).to eq(false) + end + end +end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index c25fd459dd7..1bcdc369c44 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -450,6 +450,8 @@ describe Gitlab::Database::MigrationHelpers do it 'renames a column concurrently' do allow(Gitlab::Database).to receive(:postgresql?).and_return(false) + expect(model).to receive(:check_trigger_permissions!).with(:users) + expect(model).to receive(:install_rename_triggers_for_mysql) .with(trigger_name, 'users', 'old', 'new') @@ -477,6 +479,8 @@ describe Gitlab::Database::MigrationHelpers do it 'renames a column concurrently' do allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + expect(model).to receive(:check_trigger_permissions!).with(:users) + expect(model).to receive(:install_rename_triggers_for_postgresql) .with(trigger_name, 'users', 'old', 'new') @@ -506,6 +510,8 @@ describe Gitlab::Database::MigrationHelpers do it 'cleans up the renaming procedure for PostgreSQL' do allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + expect(model).to receive(:check_trigger_permissions!).with(:users) + expect(model).to receive(:remove_rename_triggers_for_postgresql) .with(:users, /trigger_.{12}/) @@ -517,6 +523,8 @@ describe Gitlab::Database::MigrationHelpers do it 'cleans up the renaming procedure for MySQL' do allow(Gitlab::Database).to receive(:postgresql?).and_return(false) + expect(model).to receive(:check_trigger_permissions!).with(:users) + expect(model).to receive(:remove_rename_triggers_for_mysql) .with(/trigger_.{12}/) @@ -573,8 +581,8 @@ describe Gitlab::Database::MigrationHelpers do describe '#remove_rename_triggers_for_postgresql' do it 'removes the function and trigger' do - expect(model).to receive(:execute).with('DROP TRIGGER foo ON bar') - expect(model).to receive(:execute).with('DROP FUNCTION foo()') + expect(model).to receive(:execute).with('DROP TRIGGER IF EXISTS foo ON bar') + expect(model).to receive(:execute).with('DROP FUNCTION IF EXISTS foo()') model.remove_rename_triggers_for_postgresql('bar', 'foo') end @@ -582,8 +590,8 @@ describe Gitlab::Database::MigrationHelpers do describe '#remove_rename_triggers_for_mysql' do it 'removes the triggers' do - expect(model).to receive(:execute).with('DROP TRIGGER foo_insert') - expect(model).to receive(:execute).with('DROP TRIGGER foo_update') + expect(model).to receive(:execute).with('DROP TRIGGER IF EXISTS foo_insert') + expect(model).to receive(:execute).with('DROP TRIGGER IF EXISTS foo_update') model.remove_rename_triggers_for_mysql('foo') end @@ -890,4 +898,20 @@ describe Gitlab::Database::MigrationHelpers do end end end + + describe '#check_trigger_permissions!' do + it 'does nothing when the user has the correct permissions' do + expect { model.check_trigger_permissions!('users') } + .not_to raise_error(RuntimeError) + end + + it 'raises RuntimeError when the user does not have the correct permissions' do + allow(Gitlab::Database::Grant).to receive(:create_and_execute_trigger?) + .with('kittens') + .and_return(false) + + expect { model.check_trigger_permissions!('kittens') } + .to raise_error(RuntimeError, /Your database user is not allowed/) + end + end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 6b9773c9b63..4cfb4b7d357 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -433,6 +433,40 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#delete_refs' do + before(:all) do + @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + end + + it 'deletes the ref' do + @repo.delete_refs('refs/heads/feature') + + expect(@repo.rugged.references['refs/heads/feature']).to be_nil + end + + it 'deletes all refs' do + refs = %w[refs/heads/wip refs/tags/v1.1.0] + @repo.delete_refs(*refs) + + refs.each do |ref| + expect(@repo.rugged.references[ref]).to be_nil + end + end + + it 'raises an error if it failed' do + expect(Gitlab::Popen).to receive(:popen).and_return(['Error', 1]) + + expect do + @repo.delete_refs('refs/heads/fix') + end.to raise_error(Gitlab::Git::Repository::GitError) + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + end + describe "#refs_hash" do let(:refs) { repository.refs_hash } diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb new file mode 100644 index 00000000000..9d7b2136dab --- /dev/null +++ b/spec/lib/gitlab/sql/pattern_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Gitlab::SQL::Pattern do + describe '.to_pattern' do + subject(:to_pattern) { User.to_pattern(query) } + + context 'when a query is shorter than 3 chars' do + let(:query) { '12' } + + it 'returns exact matching pattern' do + expect(to_pattern).to eq('12') + end + end + + context 'when a query with a escape character is shorter than 3 chars' do + let(:query) { '_2' } + + it 'returns sanitized exact matching pattern' do + expect(to_pattern).to eq('\_2') + end + end + + context 'when a query is equal to 3 chars' do + let(:query) { '123' } + + it 'returns partial matching pattern' do + expect(to_pattern).to eq('%123%') + end + end + + context 'when a query with a escape character is equal to 3 chars' do + let(:query) { '_23' } + + it 'returns partial matching pattern' do + expect(to_pattern).to eq('%\_23%') + end + end + + context 'when a query is longer than 3 chars' do + let(:query) { '1234' } + + it 'returns partial matching pattern' do + expect(to_pattern).to eq('%1234%') + end + end + + context 'when a query with a escape character is longer than 3 chars' do + let(:query) { '_234' } + + it 'returns sanitized partial matching pattern' do + expect(to_pattern).to eq('%\_234%') + end + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2e613c44357..11717ba39e8 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1563,10 +1563,18 @@ describe Project do describe 'project import state transitions' do context 'state transition: [:started] => [:finished]' do - let(:housekeeping_service) { spy } + let(:after_import_service) { spy(:after_import_service) } + let(:housekeeping_service) { spy(:housekeeping_service) } before do - allow(Projects::HousekeepingService).to receive(:new) { housekeeping_service } + allow(Projects::AfterImportService) + .to receive(:new) { after_import_service } + + allow(after_import_service) + .to receive(:execute) { housekeeping_service.execute } + + allow(Projects::HousekeepingService) + .to receive(:new) { housekeeping_service } end it 'resets project import_error' do @@ -1581,6 +1589,7 @@ describe Project do project.import_finish + expect(after_import_service).to have_received(:execute) expect(housekeeping_service).to have_received(:execute) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 3151649b64e..34e1a955309 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -923,13 +923,16 @@ describe Repository, models: true do describe '#update_branch_with_hooks' do let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev + let(:updating_ref) { 'refs/heads/feature' } + let(:target_project) { project } + let(:target_repository) { target_project.repository } context 'when pre hooks were successful' do before do service = Gitlab::Git::HooksService.new expect(Gitlab::Git::HooksService).to receive(:new).and_return(service) expect(service).to receive(:execute) - .with(committer, repository, old_rev, new_rev, 'refs/heads/feature') + .with(committer, target_repository, old_rev, new_rev, updating_ref) .and_yield(service).and_return(true) end @@ -960,6 +963,37 @@ describe Repository, models: true do expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev) end end + + context 'when target project does not have the commit' do + let(:target_project) { create(:project, :empty_repo) } + let(:old_rev) { Gitlab::Git::BLANK_SHA } + let(:new_rev) { project.commit('feature').sha } + let(:updating_ref) { 'refs/heads/master' } + + it 'fetch_ref and create the branch' do + expect(target_project.repository).to receive(:fetch_ref) + .and_call_original + + GitOperationService.new(committer, target_repository) + .with_branch( + 'master', + start_project: project, + start_branch_name: 'feature') { new_rev } + + expect(target_repository.branch_names).to contain_exactly('master') + end + end + + context 'when target project already has the commit' do + let(:target_project) { create(:project, :repository) } + + it 'does not fetch_ref and just pass the commit' do + expect(target_repository).not_to receive(:fetch_ref) + + GitOperationService.new(committer, target_repository) + .with_branch('feature', start_project: project) { new_rev } + end + end end context 'when temporary ref failed to be created from other project' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8e04eea56a7..b70ab5581ac 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -789,6 +789,7 @@ describe User do describe '.search' do let!(:user) { create(:user, name: 'user', username: 'usern', email: 'email@gmail.com') } let!(:user2) { create(:user, name: 'user name', username: 'username', email: 'someemail@gmail.com') } + let!(:user3) { create(:user, name: 'us', username: 'se', email: 'foo@gmail.com') } describe 'name matching' do it 'returns users with a matching name with exact match first' do @@ -802,6 +803,14 @@ describe User do it 'returns users with a matching name regardless of the casing' do expect(described_class.search(user2.name.upcase)).to eq([user2]) end + + it 'returns users with a exact matching name shorter than 3 chars' do + expect(described_class.search(user3.name)).to eq([user3]) + end + + it 'returns users with a exact matching name shorter than 3 chars regardless of the casing' do + expect(described_class.search(user3.name.upcase)).to eq([user3]) + end end describe 'email matching' do @@ -830,6 +839,14 @@ describe User do it 'returns users with a matching username regardless of the casing' do expect(described_class.search(user2.username.upcase)).to eq([user2]) end + + it 'returns users with a exact matching username shorter than 3 chars' do + expect(described_class.search(user3.username)).to eq([user3]) + end + + it 'returns users with a exact matching username shorter than 3 chars regardless of the casing' do + expect(described_class.search(user3.username.upcase)).to eq([user3]) + end end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 39d76cdbc74..77c43f92456 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -444,6 +444,7 @@ describe API::Groups do expect(json_response["name"]).to eq(group[:name]) expect(json_response["path"]).to eq(group[:path]) expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled]) + expect(json_response["visibility"]).to eq(Gitlab::VisibilityLevel.string_level(Gitlab::CurrentSettings.current_application_settings.default_group_visibility)) end it "creates a nested group", :nested_groups do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 9a0c62467d3..dee75c96b86 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -509,6 +509,18 @@ describe API::Issues, :mailer do describe "GET /projects/:id/issues" do let(:base_url) { "/projects/#{project.id}" } + it 'avoids N+1 queries' do + control_count = ActiveRecord::QueryRecorder.new do + get api("/projects/#{project.id}/issues", user) + end.count + + create(:issue, author: user, project: project) + + expect do + get api("/projects/#{project.id}/issues", user) + end.not_to exceed_query_limit(control_count) + end + it 'returns 404 when project does not exist' do get api('/projects/1000/issues', non_member) diff --git a/spec/services/projects/after_import_service_spec.rb b/spec/services/projects/after_import_service_spec.rb new file mode 100644 index 00000000000..c6678fc1f5c --- /dev/null +++ b/spec/services/projects/after_import_service_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Projects::AfterImportService do + subject { described_class.new(project) } + + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + let(:sha) { project.commit.sha } + let(:housekeeping_service) { double(:housekeeping_service) } + + describe '#execute' do + before do + allow(Projects::HousekeepingService) + .to receive(:new).with(project).and_return(housekeeping_service) + + allow(housekeeping_service) + .to receive(:execute).and_yield + end + + it 'performs housekeeping' do + subject.execute + + expect(housekeeping_service).to have_received(:execute) + end + + context 'with some refs in refs/pull/**/*' do + before do + repository.write_ref('refs/pull/1/head', sha) + repository.write_ref('refs/pull/1/merge', sha) + + subject.execute + end + + it 'removes refs/pull/**/*' do + expect(repository.rugged.references.map(&:name)) + .not_to include(%r{\Arefs/pull/}) + end + end + + Repository::RESERVED_REFS_NAMES.each do |name| + context "with a ref in refs/#{name}/tmp" do + before do + repository.write_ref("refs/#{name}/tmp", sha) + + subject.execute + end + + it "does not remove refs/#{name}/tmp" do + expect(repository.rugged.references.map(&:name)) + .to include("refs/#{name}/tmp") + end + end + end + end +end diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index 385f56e447f..9386c110385 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -23,6 +23,12 @@ describe Projects::HousekeepingService do expect(project.reload.pushes_since_gc).to eq(0) end + it 'yields the block if given' do + expect do |block| + subject.execute(&block) + end.to yield_with_no_args + end + context 'when no lease can be obtained' do before do expect(subject).to receive(:try_obtain_lease).and_return(false) @@ -39,6 +45,13 @@ describe Projects::HousekeepingService do expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken) end.not_to change { project.pushes_since_gc } end + + it 'does not yield' do + expect do |block| + expect { subject.execute(&block) } + .to raise_error(Projects::HousekeepingService::LeaseTaken) + end.not_to yield_with_no_args + end end end |