diff options
author | Zeger-Jan van de Weg <zegerjan@gitlab.com> | 2017-05-07 22:35:56 +0000 |
---|---|---|
committer | Kamil Trzciński <ayufan@ayufan.eu> | 2017-05-07 22:35:56 +0000 |
commit | 8df3997a92bffa2d29f3c559933a336b837cdb93 (patch) | |
tree | 5ee50876b35b6c5fd40607665f72468cfcee51fe /spec | |
parent | 8a0cde81feb3c8f3af26eefa5cef7b72eda2d266 (diff) | |
download | gitlab-ce-8df3997a92bffa2d29f3c559933a336b837cdb93.tar.gz |
Add Pipeline Schedules that supersedes experimental Trigger Schedule
Diffstat (limited to 'spec')
21 files changed, 840 insertions, 273 deletions
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb new file mode 100644 index 00000000000..f8f95dd9bc8 --- /dev/null +++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe Projects::PipelineSchedulesController do + set(:project) { create(:empty_project, :public) } + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } + + describe 'GET #index' do + let(:scope) { nil } + let!(:inactive_pipeline_schedule) do + create(:ci_pipeline_schedule, :inactive, project: project) + end + + it 'renders the index view' do + visit_pipelines_schedules + + expect(response).to have_http_status(:ok) + expect(response).to render_template(:index) + end + + context 'when the scope is set to active' do + let(:scope) { 'active' } + + before do + visit_pipelines_schedules + end + + it 'only shows active pipeline schedules' do + expect(response).to have_http_status(:ok) + expect(assigns(:schedules)).to include(pipeline_schedule) + expect(assigns(:schedules)).not_to include(inactive_pipeline_schedule) + end + end + + def visit_pipelines_schedules + get :index, namespace_id: project.namespace.to_param, project_id: project, scope: scope + end + end + + describe 'GET edit' do + let(:user) { create(:user) } + + before do + project.add_master(user) + + sign_in(user) + end + + it 'loads the pipeline schedule' do + get :edit, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + + expect(response).to have_http_status(:ok) + expect(assigns(:schedule)).to eq(pipeline_schedule) + end + end + + describe 'DELETE #destroy' do + set(:user) { create(:user) } + + context 'when a developer makes the request' do + before do + project.add_developer(user) + sign_in(user) + + delete :destroy, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + end + + it 'does not delete the pipeline schedule' do + expect(response).not_to have_http_status(:ok) + end + end + + context 'when a master makes the request' do + before do + project.add_master(user) + sign_in(user) + end + + it 'destroys the pipeline schedule' do + expect do + delete :destroy, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + end.to change { project.pipeline_schedules.count }.by(-1) + + expect(response).to have_http_status(302) + end + end + end +end diff --git a/spec/factories/ci/trigger_schedules.rb b/spec/factories/ci/pipeline_schedule.rb index 2390706fa41..a716da46ac6 100644 --- a/spec/factories/ci/trigger_schedules.rb +++ b/spec/factories/ci/pipeline_schedule.rb @@ -1,14 +1,11 @@ FactoryGirl.define do - factory :ci_trigger_schedule, class: Ci::TriggerSchedule do - trigger factory: :ci_trigger_for_trigger_schedule + factory :ci_pipeline_schedule, class: Ci::PipelineSchedule do cron '0 1 * * *' cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE ref 'master' active true - - after(:build) do |trigger_schedule, evaluator| - trigger_schedule.project ||= trigger_schedule.trigger.project - end + description "pipeline schedule" + project factory: :empty_project trait :nightly do cron '0 1 * * *' @@ -24,5 +21,9 @@ FactoryGirl.define do cron '0 1 22 * *' cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE end + + trait :inactive do + active false + end end end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 399c1d478c5..4efd5a26a82 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb new file mode 100644 index 00000000000..cdac4fe2111 --- /dev/null +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -0,0 +1,140 @@ +require 'spec_helper' + +feature 'Pipeline Schedules', :feature do + include PipelineSchedulesHelper + include WaitForAjax + + let!(:project) { create(:project) } + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } + let(:scope) { nil } + let!(:user) { create(:user) } + + before do + project.add_master(user) + + login_as(user) + visit_page + end + + describe 'GET /projects/pipeline_schedules' do + let(:visit_page) { visit_pipelines_schedules } + + it 'avoids N + 1 queries' do + control_count = ActiveRecord::QueryRecorder.new { visit_pipelines_schedules }.count + + create_list(:ci_pipeline_schedule, 2, project: project) + + expect { visit_pipelines_schedules }.not_to exceed_query_limit(control_count) + end + + describe 'The view' do + it 'displays the required information description' do + page.within('.pipeline-schedule-table-row') do + expect(page).to have_content('pipeline schedule') + expect(page).to have_link('master') + expect(page).to have_content('None') + end + end + + it 'creates a new scheduled pipeline' do + click_link 'New Schedule' + + expect(page).to have_content('Schedule a new pipeline') + end + + it 'changes ownership of the pipeline' do + click_link 'Take ownership' + page.within('.pipeline-schedule-table-row') do + expect(page).not_to have_content('No owner') + expect(page).to have_link('John Doe') + end + end + + it 'edits the pipeline' do + page.within('.pipeline-schedule-table-row') do + click_link 'Edit' + end + + expect(page).to have_content('Edit Pipeline Schedule') + end + + it 'deletes the pipeline' do + click_link 'Delete' + + expect(page).not_to have_content('pipeline schedule') + end + end + end + + describe 'POST /projects/pipeline_schedules/new', js: true do + let(:visit_page) { visit_new_pipeline_schedule } + + it 'it creates a new scheduled pipeline' do + fill_in_schedule_form + save_pipeline_schedule + + expect(page).to have_content('my fancy description') + end + + it 'it prevents an invalid form from being submitted' do + save_pipeline_schedule + + expect(page).to have_content('This field is required') + end + end + + describe 'PATCH /projects/pipelines_schedules/:id/edit', js: true do + let(:visit_page) do + edit_pipeline_schedule + end + + it 'it displays existing properties' do + description = find_field('schedule_description').value + expect(description).to eq('pipeline schedule') + expect(page).to have_button('master') + expect(page).to have_button('UTC') + end + + it 'edits the scheduled pipeline' do + fill_in 'schedule_description', with: 'my brand new description' + + save_pipeline_schedule + + expect(page).to have_content('my brand new description') + end + end + + def visit_new_pipeline_schedule + visit new_namespace_project_pipeline_schedule_path(project.namespace, project, pipeline_schedule) + end + + def edit_pipeline_schedule + visit edit_namespace_project_pipeline_schedule_path(project.namespace, project, pipeline_schedule) + end + + def visit_pipelines_schedules + visit namespace_project_pipeline_schedules_path(project.namespace, project, scope: scope) + end + + def select_timezone + click_button 'Select a timezone' + click_link 'American Samoa' + end + + def select_target_branch + click_button 'Select target branch' + click_link 'master' + end + + def save_pipeline_schedule + click_button 'Save pipeline schedule' + end + + def fill_in_schedule_form + fill_in 'schedule_description', with: 'my fancy description' + fill_in 'schedule_cron', with: '* 1 2 3 4' + + select_timezone + select_target_branch + end +end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 26879a77c48..78a76d9c112 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe "Internal Project Access", feature: true do include AccessMatchers - let(:project) { create(:project, :internal) } + set(:project) { create(:project, :internal) } describe "Project should be internal" do describe '#internal?' do @@ -437,6 +437,20 @@ describe "Internal Project Access", feature: true do end end + describe "GET /:project_path/pipeline_schedules" do + subject { namespace_project_pipeline_schedules_path(project.namespace, project) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + describe "GET /:project_path/environments" do subject { namespace_project_environments_path(project.namespace, project) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index 699ca4f724c..a66f6e09055 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe "Private Project Access", feature: true do include AccessMatchers - let(:project) { create(:project, :private, public_builds: false) } + set(:project) { create(:project, :private, public_builds: false) } describe "Project should be private" do describe '#private?' do @@ -478,6 +478,48 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:visitor) } end + describe "GET /:project_path/pipeline_schedules" do + subject { namespace_project_pipeline_schedules_path(project.namespace, project) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + + describe "GET /:project_path/pipeline_schedules/new" do + subject { new_namespace_project_pipeline_schedule_path(project.namespace, project) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + + describe "GET /:project_path/environments/new" do + subject { new_namespace_project_pipeline_schedule_path(project.namespace, project) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + describe "GET /:project_path/container_registry" do let(:container_repository) { create(:container_repository) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 624f0d0f485..5cd575500c3 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe "Public Project Access", feature: true do include AccessMatchers - let(:project) { create(:project, :public) } + set(:project) { create(:project, :public) } describe "Project should be public" do describe '#public?' do @@ -257,6 +257,20 @@ describe "Public Project Access", feature: true do end end + describe "GET /:project_path/pipeline_schedules" do + subject { namespace_project_pipeline_schedules_path(project.namespace, project) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_allowed_for(:developer).of(project) } + it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_allowed_for(:guest).of(project) } + it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_allowed_for(:external) } + it { is_expected.to be_allowed_for(:visitor) } + end + describe "GET /:project_path/environments" do subject { namespace_project_environments_path(project.namespace, project) } diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index 783f330221c..c1ae6db00c6 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -77,77 +77,6 @@ feature 'Triggers', feature: true, js: true do expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.' expect(page.find('.triggers-list')).to have_content new_trigger_title end - - context 'scheduled triggers' do - let!(:trigger) do - create(:ci_trigger, owner: user, project: @project, description: trigger_title) - end - - context 'enabling schedule' do - before do - visit edit_namespace_project_trigger_path(@project.namespace, @project, trigger) - end - - scenario 'do fill form with valid data and save' do - find('#trigger_trigger_schedule_attributes_active').click - fill_in 'trigger_trigger_schedule_attributes_cron', with: '1 * * * *' - fill_in 'trigger_trigger_schedule_attributes_cron_timezone', with: 'UTC' - fill_in 'trigger_trigger_schedule_attributes_ref', with: 'master' - click_button 'Save trigger' - - expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.' - end - - scenario 'do not fill form with valid data and save' do - find('#trigger_trigger_schedule_attributes_active').click - click_button 'Save trigger' - - expect(page).to have_content 'The form contains the following errors' - end - - context 'when GitLab time_zone is ActiveSupport::TimeZone format' do - before do - allow(Time).to receive(:zone) - .and_return(ActiveSupport::TimeZone['Eastern Time (US & Canada)']) - end - - scenario 'do fill form with valid data and save' do - find('#trigger_trigger_schedule_attributes_active').click - fill_in 'trigger_trigger_schedule_attributes_cron', with: '1 * * * *' - fill_in 'trigger_trigger_schedule_attributes_cron_timezone', with: 'UTC' - fill_in 'trigger_trigger_schedule_attributes_ref', with: 'master' - click_button 'Save trigger' - - expect(page.find('.flash-notice')) - .to have_content 'Trigger was successfully updated.' - end - end - end - - context 'disabling schedule' do - before do - trigger.create_trigger_schedule( - project: trigger.project, - active: true, - ref: 'master', - cron: '1 * * * *', - cron_timezone: 'UTC') - - visit edit_namespace_project_trigger_path(@project.namespace, @project, trigger) - end - - scenario 'disable and save form' do - find('#trigger_trigger_schedule_attributes_active').click - click_button 'Save trigger' - expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.' - - visit edit_namespace_project_trigger_path(@project.namespace, @project, trigger) - checkbox = find_field('trigger_trigger_schedule_attributes_active') - - expect(checkbox).not_to be_checked - end - end - end end describe 'trigger "Take ownership" workflow' do diff --git a/spec/finders/pipeline_schedules_finder_spec.rb b/spec/finders/pipeline_schedules_finder_spec.rb new file mode 100644 index 00000000000..e184a87c9c7 --- /dev/null +++ b/spec/finders/pipeline_schedules_finder_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe PipelineSchedulesFinder do + let(:project) { create(:empty_project) } + + let!(:active_schedule) { create(:ci_pipeline_schedule, project: project) } + let!(:inactive_schedule) { create(:ci_pipeline_schedule, :inactive, project: project) } + + subject { described_class.new(project).execute(params) } + + describe "#execute" do + context 'when the scope is nil' do + let(:params) { { scope: nil } } + + it 'selects all pipeline pipeline schedules' do + expect(subject.count).to be(2) + expect(subject).to include(active_schedule, inactive_schedule) + end + end + + context 'when the scope is active' do + let(:params) { { scope: 'active' } } + + it 'selects only active pipelines' do + expect(subject.count).to be(1) + expect(subject).to include(active_schedule) + expect(subject).not_to include(inactive_schedule) + end + end + + context 'when the scope is inactve' do + let(:params) { { scope: 'inactive' } } + + it 'selects only inactive pipelines' do + expect(subject.count).to be(1) + expect(subject).not_to include(active_schedule) + expect(subject).to include(inactive_schedule) + end + end + end +end diff --git a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js new file mode 100644 index 00000000000..08fa6ca9057 --- /dev/null +++ b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js @@ -0,0 +1,217 @@ +import Vue from 'vue'; +import IntervalPatternInput from '~/pipeline_schedules/components/interval_pattern_input'; + +const IntervalPatternInputComponent = Vue.extend(IntervalPatternInput); +const inputNameAttribute = 'schedule[cron]'; + +const cronIntervalPresets = { + everyDay: '0 4 * * *', + everyWeek: '0 4 * * 0', + everyMonth: '0 4 1 * *', +}; + +window.gl = window.gl || {}; + +window.gl.pipelineScheduleFieldErrors = { + updateFormValidityState: () => {}, +}; + +describe('Interval Pattern Input Component', function () { + describe('when prop initialCronInterval is passed (edit)', function () { + describe('when prop initialCronInterval is custom', function () { + beforeEach(function () { + this.initialCronInterval = '1 2 3 4 5'; + this.intervalPatternComponent = new IntervalPatternInputComponent({ + propsData: { + initialCronInterval: this.initialCronInterval, + }, + }).$mount(); + }); + + it('is initialized as a Vue component', function () { + expect(this.intervalPatternComponent).toBeDefined(); + }); + + it('prop initialCronInterval is set', function () { + expect(this.intervalPatternComponent.initialCronInterval).toBe(this.initialCronInterval); + }); + + it('sets showUnsetWarning to false', function (done) { + Vue.nextTick(() => { + expect(this.intervalPatternComponent.showUnsetWarning).toBe(false); + done(); + }); + }); + + it('does not render showUnsetWarning', function (done) { + Vue.nextTick(() => { + expect(this.intervalPatternComponent.$el.outerHTML).not.toContain('Schedule not yet set'); + done(); + }); + }); + + it('sets isEditable to true', function (done) { + Vue.nextTick(() => { + expect(this.intervalPatternComponent.isEditable).toBe(true); + done(); + }); + }); + }); + + describe('when prop initialCronInterval is preset', function () { + beforeEach(function () { + this.intervalPatternComponent = new IntervalPatternInputComponent({ + propsData: { + inputNameAttribute, + initialCronInterval: '0 4 * * *', + }, + }).$mount(); + }); + + it('is initialized as a Vue component', function () { + expect(this.intervalPatternComponent).toBeDefined(); + }); + + it('sets showUnsetWarning to false', function (done) { + Vue.nextTick(() => { + expect(this.intervalPatternComponent.showUnsetWarning).toBe(false); + done(); + }); + }); + + it('does not render showUnsetWarning', function (done) { + Vue.nextTick(() => { + expect(this.intervalPatternComponent.$el.outerHTML).not.toContain('Schedule not yet set'); + done(); + }); + }); + + it('sets isEditable to false', function (done) { + Vue.nextTick(() => { + expect(this.intervalPatternComponent.isEditable).toBe(false); + done(); + }); + }); + }); + }); + + describe('when prop initialCronInterval is not passed (new)', function () { + beforeEach(function () { + this.intervalPatternComponent = new IntervalPatternInputComponent({ + propsData: { + inputNameAttribute, + }, + }).$mount(); + }); + + it('is initialized as a Vue component', function () { + expect(this.intervalPatternComponent).toBeDefined(); + }); + + it('prop initialCronInterval is set', function () { + const defaultInitialCronInterval = ''; + expect(this.intervalPatternComponent.initialCronInterval).toBe(defaultInitialCronInterval); + }); + + it('sets showUnsetWarning to true', function (done) { + Vue.nextTick(() => { + expect(this.intervalPatternComponent.showUnsetWarning).toBe(true); + done(); + }); + }); + + it('renders showUnsetWarning to true', function (done) { + Vue.nextTick(() => { + expect(this.intervalPatternComponent.$el.outerHTML).toContain('Schedule not yet set'); + done(); + }); + }); + + it('sets isEditable to true', function (done) { + Vue.nextTick(() => { + expect(this.intervalPatternComponent.isEditable).toBe(true); + done(); + }); + }); + }); + + describe('User Actions', function () { + beforeEach(function () { + // For an unknown reason, Phantom.js doesn't trigger click events + // on radio buttons in a way Vue can register. So, we have to mount + // to a fixture. + setFixtures('<div id="my-mount"></div>'); + + this.initialCronInterval = '1 2 3 4 5'; + this.intervalPatternComponent = new IntervalPatternInputComponent({ + propsData: { + initialCronInterval: this.initialCronInterval, + }, + }).$mount('#my-mount'); + }); + + it('cronInterval is updated when everyday preset interval is selected', function (done) { + this.intervalPatternComponent.$el.querySelector('#every-day').click(); + + Vue.nextTick(() => { + expect(this.intervalPatternComponent.cronInterval).toBe(cronIntervalPresets.everyDay); + expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(cronIntervalPresets.everyDay); + done(); + }); + }); + + it('cronInterval is updated when everyweek preset interval is selected', function (done) { + this.intervalPatternComponent.$el.querySelector('#every-week').click(); + + Vue.nextTick(() => { + expect(this.intervalPatternComponent.cronInterval).toBe(cronIntervalPresets.everyWeek); + expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(cronIntervalPresets.everyWeek); + + done(); + }); + }); + + it('cronInterval is updated when everymonth preset interval is selected', function (done) { + this.intervalPatternComponent.$el.querySelector('#every-month').click(); + + Vue.nextTick(() => { + expect(this.intervalPatternComponent.cronInterval).toBe(cronIntervalPresets.everyMonth); + expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(cronIntervalPresets.everyMonth); + done(); + }); + }); + + it('only a space is added to cronInterval (trimmed later) when custom radio is selected', function (done) { + this.intervalPatternComponent.$el.querySelector('#every-month').click(); + this.intervalPatternComponent.$el.querySelector('#custom').click(); + + Vue.nextTick(() => { + const intervalWithSpaceAppended = `${cronIntervalPresets.everyMonth} `; + expect(this.intervalPatternComponent.cronInterval).toBe(intervalWithSpaceAppended); + expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(intervalWithSpaceAppended); + done(); + }); + }); + + it('text input is disabled when preset interval is selected', function (done) { + this.intervalPatternComponent.$el.querySelector('#every-month').click(); + + Vue.nextTick(() => { + expect(this.intervalPatternComponent.isEditable).toBe(false); + expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').disabled).toBe(true); + done(); + }); + }); + + it('text input is enabled when custom is selected', function (done) { + this.intervalPatternComponent.$el.querySelector('#every-month').click(); + this.intervalPatternComponent.$el.querySelector('#custom').click(); + + Vue.nextTick(() => { + expect(this.intervalPatternComponent.isEditable).toBe(true); + expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').disabled).toBe(false); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js b/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js new file mode 100644 index 00000000000..1d05f37cb36 --- /dev/null +++ b/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js @@ -0,0 +1,91 @@ +import Vue from 'vue'; +import Cookies from 'js-cookie'; +import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_schedules_callout'; + +const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout); +const cookieKey = 'pipeline_schedules_callout_dismissed'; + +describe('Pipeline Schedule Callout', () => { + describe('independent of cookies', () => { + beforeEach(() => { + this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount(); + }); + + it('the component can be initialized', () => { + expect(this.calloutComponent).toBeDefined(); + }); + + it('correctly sets illustrationSvg', () => { + expect(this.calloutComponent.illustrationSvg).toContain('<svg'); + }); + }); + + describe(`when ${cookieKey} cookie is set`, () => { + beforeEach(() => { + Cookies.set(cookieKey, true); + this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount(); + }); + + it('correctly sets calloutDismissed to true', () => { + expect(this.calloutComponent.calloutDismissed).toBe(true); + }); + + it('does not render the callout', () => { + expect(this.calloutComponent.$el.childNodes.length).toBe(0); + }); + }); + + describe('when cookie is not set', () => { + beforeEach(() => { + Cookies.remove(cookieKey); + this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount(); + }); + + it('correctly sets calloutDismissed to false', () => { + expect(this.calloutComponent.calloutDismissed).toBe(false); + }); + + it('renders the callout container', () => { + expect(this.calloutComponent.$el.querySelector('.bordered-box')).not.toBeNull(); + }); + + it('renders the callout svg', () => { + expect(this.calloutComponent.$el.outerHTML).toContain('<svg'); + }); + + it('renders the callout title', () => { + expect(this.calloutComponent.$el.outerHTML).toContain('Scheduling Pipelines'); + }); + + it('renders the callout text', () => { + expect(this.calloutComponent.$el.outerHTML).toContain('runs pipelines in the future'); + }); + + it('updates calloutDismissed when close button is clicked', (done) => { + this.calloutComponent.$el.querySelector('#dismiss-callout-btn').click(); + + Vue.nextTick(() => { + expect(this.calloutComponent.calloutDismissed).toBe(true); + done(); + }); + }); + + it('#dismissCallout updates calloutDismissed', (done) => { + this.calloutComponent.dismissCallout(); + + Vue.nextTick(() => { + expect(this.calloutComponent.calloutDismissed).toBe(true); + done(); + }); + }); + + it('is hidden when close button is clicked', (done) => { + this.calloutComponent.$el.querySelector('#dismiss-callout-btn').click(); + + Vue.nextTick(() => { + expect(this.calloutComponent.$el.childNodes.length).toBe(0); + done(); + }); + }); + }); +}); diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index baa81870e81..688e731bf15 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -101,6 +101,7 @@ pipelines: - cancelable_statuses - manual_actions - artifacts +- pipeline_schedule statuses: - project - pipeline @@ -112,9 +113,13 @@ triggers: - project - trigger_requests - owner -- trigger_schedule -trigger_schedule: -- trigger +pipeline_schedules: +- project +- owner +- pipelines +- last_pipeline +pipeline_schedule: +- pipelines deploy_keys: - user - deploy_keys_projects @@ -221,7 +226,7 @@ project: - active_runners - variables - triggers -- trigger_schedules +- pipeline_schedules - environments - deployments - project_feature diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index a66086f8b47..3af2a172e6d 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -188,6 +188,7 @@ Ci::Pipeline: - user_id - lock_version - auto_canceled_by_id +- pipeline_schedule_id CommitStatus: - id - project_id @@ -247,18 +248,19 @@ Ci::Trigger: - owner_id - description - ref -Ci::TriggerSchedule: +Ci::PipelineSchedule: - id -- project_id -- trigger_id -- deleted_at -- created_at -- updated_at +- description +- ref - cron - cron_timezone - next_run_at -- ref +- project_id +- owner_id - active +- deleted_at +- created_at +- updated_at DeployKey: - id - user_id diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index bf1dfe7f412..9046d5c413f 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -32,6 +32,7 @@ describe Gitlab::UsageData do ci_pipelines ci_runners ci_triggers + ci_pipeline_schedules deploy_keys deployments environments diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb new file mode 100644 index 00000000000..822b98c5f6c --- /dev/null +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' + +describe Ci::PipelineSchedule, models: true do + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:owner) } + + it { is_expected.to have_many(:pipelines) } + + it { is_expected.to respond_to(:ref) } + it { is_expected.to respond_to(:cron) } + it { is_expected.to respond_to(:cron_timezone) } + it { is_expected.to respond_to(:description) } + it { is_expected.to respond_to(:next_run_at) } + it { is_expected.to respond_to(:deleted_at) } + + describe 'validations' do + it 'does not allow invalid cron patters' do + pipeline_schedule = build(:ci_pipeline_schedule, cron: '0 0 0 * *') + + expect(pipeline_schedule).not_to be_valid + end + + it 'does not allow invalid cron patters' do + pipeline_schedule = build(:ci_pipeline_schedule, cron_timezone: 'invalid') + + expect(pipeline_schedule).not_to be_valid + end + end + + describe '#set_next_run_at' do + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) } + + context 'when creates new pipeline schedule' do + let(:expected_next_run_at) do + Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone). + next_time_from(Time.now) + end + + it 'updates next_run_at automatically' do + expect(Ci::PipelineSchedule.last.next_run_at).to eq(expected_next_run_at) + end + end + + context 'when updates cron of exsisted pipeline schedule' do + let(:new_cron) { '0 0 1 1 *' } + + let(:expected_next_run_at) do + Gitlab::Ci::CronParser.new(new_cron, pipeline_schedule.cron_timezone). + next_time_from(Time.now) + end + + it 'updates next_run_at automatically' do + pipeline_schedule.update!(cron: new_cron) + + expect(Ci::PipelineSchedule.last.next_run_at).to eq(expected_next_run_at) + end + end + end + + describe '#schedule_next_run!' do + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) } + + context 'when reschedules after 10 days from now' do + let(:future_time) { 10.days.from_now } + + let(:expected_next_run_at) do + Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone). + next_time_from(future_time) + end + + it 'points to proper next_run_at' do + Timecop.freeze(future_time) do + pipeline_schedule.schedule_next_run! + + expect(pipeline_schedule.next_run_at).to eq(expected_next_run_at) + end + end + end + end + + describe '#real_next_run' do + subject do + described_class.last.real_next_run(worker_cron: worker_cron, + worker_time_zone: worker_time_zone) + end + + context 'when GitLab time_zone is UTC' do + before do + allow(Time).to receive(:zone) + .and_return(ActiveSupport::TimeZone[worker_time_zone]) + end + + let(:worker_time_zone) { 'UTC' } + + context 'when cron_timezone is Eastern Time (US & Canada)' do + before do + create(:ci_pipeline_schedule, :nightly, + cron_timezone: 'Eastern Time (US & Canada)') + end + + let(:worker_cron) { '0 1 2 3 *' } + + it 'returns the next time worker executes' do + expect(subject.min).to eq(0) + expect(subject.hour).to eq(1) + expect(subject.day).to eq(2) + expect(subject.month).to eq(3) + end + end + end + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3b222ea1c3d..208c8cb1c3d 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -13,6 +13,7 @@ describe Ci::Pipeline, models: true do it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:auto_canceled_by) } + it { is_expected.to belong_to(:pipeline_schedule) } it { is_expected.to have_many(:statuses) } it { is_expected.to have_many(:trigger_requests) } diff --git a/spec/models/ci/trigger_schedule_spec.rb b/spec/models/ci/trigger_schedule_spec.rb deleted file mode 100644 index 92447564d7c..00000000000 --- a/spec/models/ci/trigger_schedule_spec.rb +++ /dev/null @@ -1,108 +0,0 @@ -require 'spec_helper' - -describe Ci::TriggerSchedule, models: true do - it { is_expected.to belong_to(:project) } - it { is_expected.to belong_to(:trigger) } - it { is_expected.to respond_to(:ref) } - - describe '#set_next_run_at' do - context 'when creates new TriggerSchedule' do - before do - trigger_schedule = create(:ci_trigger_schedule, :nightly) - @expected_next_run_at = Gitlab::Ci::CronParser.new(trigger_schedule.cron, trigger_schedule.cron_timezone) - .next_time_from(Time.now) - end - - it 'updates next_run_at automatically' do - expect(Ci::TriggerSchedule.last.next_run_at).to eq(@expected_next_run_at) - end - end - - context 'when updates cron of exsisted TriggerSchedule' do - before do - trigger_schedule = create(:ci_trigger_schedule, :nightly) - new_cron = '0 0 1 1 *' - trigger_schedule.update!(cron: new_cron) # Subject - @expected_next_run_at = Gitlab::Ci::CronParser.new(new_cron, trigger_schedule.cron_timezone) - .next_time_from(Time.now) - end - - it 'updates next_run_at automatically' do - expect(Ci::TriggerSchedule.last.next_run_at).to eq(@expected_next_run_at) - end - end - end - - describe '#schedule_next_run!' do - context 'when reschedules after 10 days from now' do - before do - trigger_schedule = create(:ci_trigger_schedule, :nightly) - time_future = Time.now + 10.days - allow(Time).to receive(:now).and_return(time_future) - trigger_schedule.schedule_next_run! # Subject - @expected_next_run_at = Gitlab::Ci::CronParser.new(trigger_schedule.cron, trigger_schedule.cron_timezone) - .next_time_from(time_future) - end - - it 'points to proper next_run_at' do - expect(Ci::TriggerSchedule.last.next_run_at).to eq(@expected_next_run_at) - end - end - - context 'when cron is invalid' do - before do - trigger_schedule = create(:ci_trigger_schedule, :nightly) - trigger_schedule.cron = 'Invalid-cron' - trigger_schedule.schedule_next_run! # Subject - end - - it 'sets nil to next_run_at' do - expect(Ci::TriggerSchedule.last.next_run_at).to be_nil - end - end - - context 'when cron_timezone is invalid' do - before do - trigger_schedule = create(:ci_trigger_schedule, :nightly) - trigger_schedule.cron_timezone = 'Invalid-cron_timezone' - trigger_schedule.schedule_next_run! # Subject - end - - it 'sets nil to next_run_at' do - expect(Ci::TriggerSchedule.last.next_run_at).to be_nil - end - end - end - - describe '#real_next_run' do - subject do - Ci::TriggerSchedule.last.real_next_run(worker_cron: worker_cron, - worker_time_zone: worker_time_zone) - end - - context 'when GitLab time_zone is UTC' do - before do - allow(Time).to receive(:zone) - .and_return(ActiveSupport::TimeZone[worker_time_zone]) - end - - let(:worker_time_zone) { 'UTC' } - - context 'when cron_timezone is Eastern Time (US & Canada)' do - before do - create(:ci_trigger_schedule, :nightly, - cron_timezone: 'Eastern Time (US & Canada)') - end - - let(:worker_cron) { '0 1 2 3 *' } - - it 'returns the next time worker executes' do - expect(subject.min).to eq(0) - expect(subject.hour).to eq(1) - expect(subject.day).to eq(2) - expect(subject.month).to eq(3) - end - end - end - end -end diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb index d26121018ce..92c15c13c18 100644 --- a/spec/models/ci/trigger_spec.rb +++ b/spec/models/ci/trigger_spec.rb @@ -7,7 +7,6 @@ describe Ci::Trigger, models: true do it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:owner) } it { is_expected.to have_many(:trigger_requests) } - it { is_expected.to have_one(:trigger_schedule) } end describe 'before_validation' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2fc8ffed80a..429b3dd83af 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -73,6 +73,7 @@ describe Project, models: true do it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:forks).through(:forked_project_links) } it { is_expected.to have_many(:uploads).dependent(:destroy) } + it { is_expected.to have_many(:pipeline_schedules).dependent(:destroy) } context 'after initialized' do it "has a project_feature" do diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb new file mode 100644 index 00000000000..91d5a16993f --- /dev/null +++ b/spec/workers/pipeline_schedule_worker_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe PipelineScheduleWorker do + subject { described_class.new.perform } + + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + + let!(:pipeline_schedule) do + create(:ci_pipeline_schedule, :nightly, project: project, owner: user) + end + + before do + project.add_master(user) + + stub_ci_pipeline_to_return_yaml_file + end + + context 'when there is a scheduled pipeline within next_run_at' do + let(:next_run_at) { 2.days.ago } + + before do + pipeline_schedule.update_column(:next_run_at, next_run_at) + end + + it 'creates a new pipeline' do + expect { subject }.to change { project.pipelines.count }.by(1) + end + + it 'updates the next_run_at field' do + subject + + expect(pipeline_schedule.reload.next_run_at).to be > Time.now + end + + it 'sets the schedule on the pipeline' do + subject + expect(project.pipelines.last.pipeline_schedule).to eq(pipeline_schedule) + end + end + + context 'inactive schedule' do + before do + pipeline_schedule.update(active: false) + end + + it 'does not creates a new pipeline' do + expect { subject }.not_to change { project.pipelines.count } + end + end +end diff --git a/spec/workers/trigger_schedule_worker_spec.rb b/spec/workers/trigger_schedule_worker_spec.rb deleted file mode 100644 index 861bed4442e..00000000000 --- a/spec/workers/trigger_schedule_worker_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'spec_helper' - -describe TriggerScheduleWorker do - let(:worker) { described_class.new } - - before do - stub_ci_pipeline_to_return_yaml_file - end - - context 'when there is a scheduled trigger within next_run_at' do - let(:next_run_at) { 2.days.ago } - - let!(:trigger_schedule) do - create(:ci_trigger_schedule, :nightly) - end - - before do - trigger_schedule.update_column(:next_run_at, next_run_at) - end - - it 'creates a new trigger request' do - expect { worker.perform }.to change { Ci::TriggerRequest.count } - end - - it 'creates a new pipeline' do - expect { worker.perform }.to change { Ci::Pipeline.count } - expect(Ci::Pipeline.last).to be_pending - end - - it 'updates next_run_at' do - worker.perform - - expect(trigger_schedule.reload.next_run_at).not_to eq(next_run_at) - end - - context 'inactive schedule' do - before do - trigger_schedule.update(active: false) - end - - it 'does not create a new trigger' do - expect { worker.perform }.not_to change { Ci::TriggerRequest.count } - end - end - end - - context 'when there are no scheduled triggers within next_run_at' do - before { create(:ci_trigger_schedule, :nightly) } - - it 'does not create a new pipeline' do - expect { worker.perform }.not_to change { Ci::Pipeline.count } - end - - it 'does not update next_run_at' do - expect { worker.perform }.not_to change { Ci::TriggerSchedule.last.next_run_at } - end - end - - context 'when next_run_at is nil' do - before do - schedule = create(:ci_trigger_schedule, :nightly) - schedule.update_column(:next_run_at, nil) - end - - it 'does not create a new pipeline' do - expect { worker.perform }.not_to change { Ci::Pipeline.count } - end - - it 'does not update next_run_at' do - expect { worker.perform }.not_to change { Ci::TriggerSchedule.last.next_run_at } - end - end -end |