Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f1bb2a307e
commit
170f0bdcde
13 changed files with 290 additions and 1 deletions
|
@ -1,7 +1,6 @@
|
|||
.except-deploys:
|
||||
except:
|
||||
refs:
|
||||
- /^[\d-]+-stable(-ee)?$/
|
||||
- /^\d+-\d+-auto-deploy-\d+$/
|
||||
|
||||
.review-docker:
|
||||
|
|
17
app/models/zoom_meeting.rb
Normal file
17
app/models/zoom_meeting.rb
Normal file
|
@ -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
|
21
app/validators/same_project_association_validator.rb
Normal file
21
app/validators/same_project_association_validator.rb
Normal file
|
@ -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
|
13
app/validators/zoom_url_validator.rb
Normal file
13
app/validators/zoom_url_validator.rb
Normal file
|
@ -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
|
24
db/migrate/20190930153535_create_zoom_meetings.rb
Normal file
24
db/migrate/20190930153535_create_zoom_meetings.rb
Normal file
|
@ -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
|
14
db/schema.rb
14
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
|
||||
|
|
1
qa/qa.rb
1
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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
21
qa/qa/specs/loop_runner.rb
Normal file
21
qa/qa/specs/loop_runner.rb
Normal file
|
@ -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
|
|
@ -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?
|
||||
|
|
18
spec/factories/zoom_meetings.rb
Normal file
18
spec/factories/zoom_meetings.rb
Normal file
|
@ -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
|
154
spec/models/zoom_meeting_spec.rb
Normal file
154
spec/models/zoom_meeting_spec.rb
Normal file
|
@ -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
|
Loading…
Reference in a new issue