summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-10-21 21:06:14 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-10-21 21:06:14 +0000
commit170f0bdcdef9c9b226abfe0a50d6687c65e8d613 (patch)
tree5c82769a5380a0fd495bd1adb098c8c360334587
parentf1bb2a307e9b125a8ee0be3728cb0d1baa21a3d4 (diff)
downloadgitlab-ce-170f0bdcdef9c9b226abfe0a50d6687c65e8d613.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml1
-rw-r--r--app/models/zoom_meeting.rb17
-rw-r--r--app/validators/same_project_association_validator.rb21
-rw-r--r--app/validators/zoom_url_validator.rb13
-rw-r--r--db/migrate/20190930153535_create_zoom_meetings.rb24
-rw-r--r--db/schema.rb14
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/runtime/env.rb4
-rw-r--r--qa/qa/scenario/shared_attributes.rb1
-rw-r--r--qa/qa/specs/loop_runner.rb21
-rw-r--r--qa/qa/specs/runner.rb2
-rw-r--r--spec/factories/zoom_meetings.rb18
-rw-r--r--spec/models/zoom_meeting_spec.rb154
13 files changed, 290 insertions, 1 deletions
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 09cf38908a6..fd26711cfcf 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -1,7 +1,6 @@
.except-deploys:
except:
refs:
- - /^[\d-]+-stable(-ee)?$/
- /^\d+-\d+-auto-deploy-\d+$/
.review-docker:
diff --git a/app/models/zoom_meeting.rb b/app/models/zoom_meeting.rb
new file mode 100644
index 00000000000..cb3b5c60e54
--- /dev/null
+++ b/app/models/zoom_meeting.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class ZoomMeeting < ApplicationRecord
+ belongs_to :project, optional: false
+ belongs_to :issue, optional: false
+
+ validates :url, presence: true, length: { maximum: 255 }, zoom_url: true
+ validates :issue, same_project_association: true
+
+ enum issue_status: {
+ added: 1,
+ removed: 2
+ }
+
+ scope :added_to_issue, -> { where(issue_status: :added) }
+ scope :removed_from_issue, -> { where(issue_status: :removed) }
+end
diff --git a/app/validators/same_project_association_validator.rb b/app/validators/same_project_association_validator.rb
new file mode 100644
index 00000000000..2af2a21fa9a
--- /dev/null
+++ b/app/validators/same_project_association_validator.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# SameProjectAssociationValidator
+#
+# Custom validator to validate that the same project associated with
+# the record is also associated with the value
+#
+# Example:
+# class ZoomMeeting < ApplicationRecord
+# belongs_to :project, optional: false
+# belongs_to :issue, optional: false
+
+# validates :issue, same_project_association: true
+# end
+class SameProjectAssociationValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if record.project == value&.project
+
+ record.errors[attribute] << 'must associate the same project'
+ end
+end
diff --git a/app/validators/zoom_url_validator.rb b/app/validators/zoom_url_validator.rb
new file mode 100644
index 00000000000..dc4ca6b9501
--- /dev/null
+++ b/app/validators/zoom_url_validator.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# ZoomUrlValidator
+#
+# Custom validator for zoom urls
+#
+class ZoomUrlValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if Gitlab::ZoomLinkExtractor.new(value).links.size == 1
+
+ record.errors.add(:url, 'must contain one valid Zoom URL')
+ end
+end
diff --git a/db/migrate/20190930153535_create_zoom_meetings.rb b/db/migrate/20190930153535_create_zoom_meetings.rb
new file mode 100644
index 00000000000..6b92c53da79
--- /dev/null
+++ b/db/migrate/20190930153535_create_zoom_meetings.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class CreateZoomMeetings < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ ZOOM_MEETING_STATUS_ADDED = 1
+
+ def change
+ create_table :zoom_meetings do |t|
+ t.references :project, foreign_key: { on_delete: :cascade },
+ null: false
+ t.references :issue, foreign_key: { on_delete: :cascade },
+ null: false
+ t.timestamps_with_timezone null: false
+ t.integer :issue_status, limit: 2, default: 1, null: false
+ t.string :url, limit: 255
+
+ t.index [:issue_id, :issue_status], unique: true,
+ where: "issue_status = #{ZOOM_MEETING_STATUS_ADDED}"
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 37e540a9b3a..109f9e8e038 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -3992,6 +3992,18 @@ ActiveRecord::Schema.define(version: 2019_10_16_220135) do
t.index ["type"], name: "index_web_hooks_on_type"
end
+ create_table "zoom_meetings", force: :cascade do |t|
+ t.bigint "project_id", null: false
+ t.bigint "issue_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "issue_status", limit: 2, default: 1, null: false
+ t.string "url", limit: 255
+ t.index ["issue_id", "issue_status"], name: "index_zoom_meetings_on_issue_id_and_issue_status", unique: true, where: "(issue_status = 1)"
+ t.index ["issue_id"], name: "index_zoom_meetings_on_issue_id"
+ t.index ["project_id"], name: "index_zoom_meetings_on_project_id"
+ end
+
add_foreign_key "alerts_service_data", "services", on_delete: :cascade
add_foreign_key "allowed_email_domains", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "analytics_cycle_analytics_group_stages", "labels", column: "end_event_label_id", on_delete: :cascade
@@ -4406,4 +4418,6 @@ ActiveRecord::Schema.define(version: 2019_10_16_220135) do
add_foreign_key "vulnerability_scanners", "projects", on_delete: :cascade
add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade
add_foreign_key "web_hooks", "projects", name: "fk_0c8ca6d9d1", on_delete: :cascade
+ add_foreign_key "zoom_meetings", "issues", on_delete: :cascade
+ add_foreign_key "zoom_meetings", "projects", on_delete: :cascade
end
diff --git a/qa/qa.rb b/qa/qa.rb
index a628c0e0e3f..f9fba28bacb 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -419,6 +419,7 @@ module QA
autoload :Config, 'qa/specs/config'
autoload :Runner, 'qa/specs/runner'
autoload :ParallelRunner, 'qa/specs/parallel_runner'
+ autoload :LoopRunner, 'qa/specs/loop_runner'
module Helpers
autoload :Quarantine, 'qa/specs/helpers/quarantine'
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index b4047ef5088..bcd2a225469 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -261,6 +261,10 @@ module QA
ENV['QA_RUNTIME_SCENARIO_ATTRIBUTES']
end
+ def gitlab_qa_loop_runner_minutes
+ ENV.fetch('GITLAB_QA_LOOP_RUNNER_MINUTES', 1).to_i
+ end
+
private
def remote_grid_credentials
diff --git a/qa/qa/scenario/shared_attributes.rb b/qa/qa/scenario/shared_attributes.rb
index 52f50ec8c27..bb45c4ce4cb 100644
--- a/qa/qa/scenario/shared_attributes.rb
+++ b/qa/qa/scenario/shared_attributes.rb
@@ -8,6 +8,7 @@ module QA
attribute :gitlab_address, '--address URL', 'Address of the instance to test'
attribute :enable_feature, '--enable-feature FEATURE_FLAG', 'Enable a feature before running tests'
attribute :parallel, '--parallel', 'Execute tests in parallel'
+ attribute :loop, '--loop', 'Execute test repeatedly'
end
end
end
diff --git a/qa/qa/specs/loop_runner.rb b/qa/qa/specs/loop_runner.rb
new file mode 100644
index 00000000000..f97f5cbbd81
--- /dev/null
+++ b/qa/qa/specs/loop_runner.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module QA
+ module Specs
+ module LoopRunner
+ module_function
+
+ def run(args)
+ start = Time.now
+ loop_duration = 60 * QA::Runtime::Env.gitlab_qa_loop_runner_minutes
+
+ while Time.now - start < loop_duration
+ RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
+ abort if status.nonzero?
+ end
+ RSpec.clear_examples
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index 6aa08cf77b4..ac73cc00dbf 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -63,6 +63,8 @@ module QA
if Runtime::Scenario.attributes[:parallel]
ParallelRunner.run(args.flatten)
+ elsif Runtime::Scenario.attributes[:loop]
+ LoopRunner.run(args.flatten)
else
RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
abort if status.nonzero?
diff --git a/spec/factories/zoom_meetings.rb b/spec/factories/zoom_meetings.rb
new file mode 100644
index 00000000000..b280deca012
--- /dev/null
+++ b/spec/factories/zoom_meetings.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :zoom_meeting do
+ project { issue.project }
+ issue
+ url { 'https://zoom.us/j/123456789' }
+ issue_status { :added }
+
+ trait :added_to_issue do
+ issue_status { :added }
+ end
+
+ trait :removed_from_issue do
+ issue_status { :removed }
+ end
+ end
+end
diff --git a/spec/models/zoom_meeting_spec.rb b/spec/models/zoom_meeting_spec.rb
new file mode 100644
index 00000000000..3dad957a1ce
--- /dev/null
+++ b/spec/models/zoom_meeting_spec.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ZoomMeeting do
+ let(:project) { build(:project) }
+
+ describe 'Factory' do
+ subject { build(:zoom_meeting) }
+
+ it { is_expected.to be_valid }
+ end
+
+ describe 'Associations' do
+ it { is_expected.to belong_to(:project).required }
+ it { is_expected.to belong_to(:issue).required }
+ end
+
+ describe 'scopes' do
+ let(:issue) { create(:issue, project: project) }
+ let!(:added_meeting) { create(:zoom_meeting, :added_to_issue, issue: issue) }
+ let!(:removed_meeting) { create(:zoom_meeting, :removed_from_issue, issue: issue) }
+
+ describe '.added_to_issue' do
+ it 'gets only added meetings' do
+ meetings_added = described_class.added_to_issue.pluck(:id)
+
+ expect(meetings_added).to include(added_meeting.id)
+ expect(meetings_added).not_to include(removed_meeting.id)
+ end
+ end
+ describe '.removed_from_issue' do
+ it 'gets only removed meetings' do
+ meetings_removed = described_class.removed_from_issue.pluck(:id)
+
+ expect(meetings_removed).to include(removed_meeting.id)
+ expect(meetings_removed).not_to include(added_meeting.id)
+ end
+ end
+ end
+
+ describe 'Validations' do
+ describe 'url' do
+ it { is_expected.to validate_presence_of(:url) }
+ it { is_expected.to validate_length_of(:url).is_at_most(255) }
+
+ shared_examples 'invalid Zoom URL' do
+ it do
+ expect(subject).to be_invalid
+ expect(subject.errors[:url])
+ .to contain_exactly('must contain one valid Zoom URL')
+ end
+ end
+
+ context 'with non-Zoom URL' do
+ before do
+ subject.url = %{https://non-zoom.url}
+ end
+
+ include_examples 'invalid Zoom URL'
+ end
+
+ context 'with multiple Zoom-URLs' do
+ before do
+ subject.url = %{https://zoom.us/j/123 https://zoom.us/j/456}
+ end
+
+ include_examples 'invalid Zoom URL'
+ end
+ end
+
+ describe 'issue association' do
+ let(:issue) { build(:issue, project: project) }
+
+ subject { build(:zoom_meeting, project: project, issue: issue) }
+
+ context 'for the same project' do
+ it { is_expected.to be_valid }
+ end
+
+ context 'for a different project' do
+ let(:issue) { build(:issue) }
+
+ it do
+ expect(subject).to be_invalid
+ expect(subject.errors[:issue])
+ .to contain_exactly('must associate the same project')
+ end
+ end
+ end
+ end
+
+ describe 'limit number of meetings per issue' do
+ shared_examples 'can add meetings' do
+ it 'can add new Zoom meetings' do
+ create(:zoom_meeting, :added_to_issue, issue: issue)
+ end
+ end
+
+ shared_examples 'can remove meetings' do
+ it 'can remove Zoom meetings' do
+ create(:zoom_meeting, :removed_from_issue, issue: issue)
+ end
+ end
+
+ shared_examples 'cannot add meetings' do
+ it 'fails to add a new meeting' do
+ expect do
+ create(:zoom_meeting, :added_to_issue, issue: issue)
+ end.to raise_error ActiveRecord::RecordNotUnique
+ end
+ end
+
+ let(:issue) { create(:issue, project: project) }
+
+ context 'without meetings' do
+ it_behaves_like 'can add meetings'
+ end
+
+ context 'when no other meeting is added' do
+ before do
+ create(:zoom_meeting, :removed_from_issue, issue: issue)
+ end
+
+ it_behaves_like 'can add meetings'
+ end
+
+ context 'when meeting is added' do
+ before do
+ create(:zoom_meeting, :added_to_issue, issue: issue)
+ end
+
+ it_behaves_like 'cannot add meetings'
+ end
+
+ context 'when meeting is added to another issue' do
+ let(:another_issue) { create(:issue, project: project) }
+
+ before do
+ create(:zoom_meeting, :added_to_issue, issue: another_issue)
+ end
+
+ it_behaves_like 'can add meetings'
+ end
+
+ context 'when second meeting is removed' do
+ before do
+ create(:zoom_meeting, :removed_from_issue, issue: issue)
+ end
+
+ it_behaves_like 'can remove meetings'
+ end
+ end
+end