summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorZeger-Jan van de Weg <zegerjan@gitlab.com>2017-05-07 22:35:56 +0000
committerKamil Trzciński <ayufan@ayufan.eu>2017-05-07 22:35:56 +0000
commit8df3997a92bffa2d29f3c559933a336b837cdb93 (patch)
tree5ee50876b35b6c5fd40607665f72468cfcee51fe /spec
parent8a0cde81feb3c8f3af26eefa5cef7b72eda2d266 (diff)
downloadgitlab-ce-8df3997a92bffa2d29f3c559933a336b837cdb93.tar.gz
Add Pipeline Schedules that supersedes experimental Trigger Schedule
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb87
-rw-r--r--spec/factories/ci/pipeline_schedule.rb (renamed from spec/factories/ci/trigger_schedules.rb)13
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin679892 -> 681478 bytes
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb140
-rw-r--r--spec/features/security/project/internal_access_spec.rb16
-rw-r--r--spec/features/security/project/private_access_spec.rb44
-rw-r--r--spec/features/security/project/public_access_spec.rb16
-rw-r--r--spec/features/triggers_spec.rb71
-rw-r--r--spec/finders/pipeline_schedules_finder_spec.rb41
-rw-r--r--spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js217
-rw-r--r--spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js91
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml13
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml16
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb1
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb112
-rw-r--r--spec/models/ci/pipeline_spec.rb1
-rw-r--r--spec/models/ci/trigger_schedule_spec.rb108
-rw-r--r--spec/models/ci/trigger_spec.rb1
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb51
-rw-r--r--spec/workers/trigger_schedule_worker_spec.rb73
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
index 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
Binary files differ
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