Commit 5c34fbfb authored by Peter Leitzen's avatar Peter Leitzen Committed by allison.browne

Add table and model for `zoom_meetings`

Add Active Record backed class `ZoomMeeting`s to persist and validate
zoom links associated to an issue. We also associate meetings to a
project (via project_id) to avoid table joins. `ZoomMeeting`s can have
an issue_status of `added` or `removed` which will be used to show or
hide the zoom link on an issue via quick actions.
parent c35745fd
# frozen_string_literal: true
class ZoomMeeting < ApplicationRecord
belongs_to :project, required: true
belongs_to :issue, required: true
validates :url, presence: true, length: { maximum: 255 }
validate :check_zoom_url
validate :check_issue_association
enum issue_status: {
added: 1,
removed: 2
}
scope :added_to_issue, -> { where(issue_status: :added) }
scope :removed_from_issue, -> { where(issue_status: :removed) }
private
def check_zoom_url
return if Gitlab::ZoomLinkExtractor.new(url).links.size == 1
errors.add(:url, 'must contain one valid Zoom URL')
end
def check_issue_association
return if project == issue&.project
errors.add(:issue, 'must associate the same project')
end
end
# 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.integer :project_id, null: false, index: true
t.integer :issue_id, null: false, index: true
t.timestamps_with_timezone null: false
t.integer :issue_status, limit: 2, default: 1, null: false
t.string :url, limit: 255
t.foreign_key :projects, on_delete: :cascade
t.foreign_key :issues, on_delete: :cascade
t.index [:issue_id, :issue_status], unique: true,
where: "issue_status = #{ZOOM_MEETING_STATUS_ADDED}"
end
end
end
......@@ -3897,6 +3897,18 @@ ActiveRecord::Schema.define(version: 2019_10_04_133612) do
t.index ["type"], name: "index_web_hooks_on_type"
end
create_table "zoom_meetings", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "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
......@@ -4299,4 +4311,6 @@ ActiveRecord::Schema.define(version: 2019_10_04_133612) 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
# 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
# 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 '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 '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
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment