From 170f0bdcdef9c9b226abfe0a50d6687c65e8d613 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 21 Oct 2019 21:06:14 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/review.gitlab-ci.yml | 1 - app/models/zoom_meeting.rb | 17 ++ .../same_project_association_validator.rb | 21 +++ app/validators/zoom_url_validator.rb | 13 ++ .../20190930153535_create_zoom_meetings.rb | 24 +++ db/schema.rb | 14 ++ qa/qa.rb | 1 + qa/qa/runtime/env.rb | 4 + qa/qa/scenario/shared_attributes.rb | 1 + qa/qa/specs/loop_runner.rb | 21 +++ qa/qa/specs/runner.rb | 2 + spec/factories/zoom_meetings.rb | 18 ++ spec/models/zoom_meeting_spec.rb | 154 ++++++++++++++++++ 13 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 app/models/zoom_meeting.rb create mode 100644 app/validators/same_project_association_validator.rb create mode 100644 app/validators/zoom_url_validator.rb create mode 100644 db/migrate/20190930153535_create_zoom_meetings.rb create mode 100644 qa/qa/specs/loop_runner.rb create mode 100644 spec/factories/zoom_meetings.rb create mode 100644 spec/models/zoom_meeting_spec.rb 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