From 8e254665e0db3bed2d9b91f12063c7b6a6d73dc2 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 18 Feb 2025 06:10:57 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../components/add_request.vue | 35 +++-- .../concerns/integrations/base/telegram.rb | 134 ++++++++++++++++++ app/models/integrations/instance/telegram.rb | 2 +- app/models/integrations/telegram.rb | 121 +--------------- .../approval_merge_request_rule_sources.yml | 12 +- ..._sources_project_id_not_null_constraint.rb | 13 ++ db/schema_migrations/20250210064812 | 1 + db/structure.sql | 6 +- doc/development/fe_guide/vue.md | 6 + .../components/add_request_spec.js | 7 +- .../integrations/instance/telegram_spec.rb | 7 + spec/models/integrations/telegram_spec.rb | 96 +------------ .../base/telegram_shared_examples.rb | 102 +++++++++++++ 13 files changed, 300 insertions(+), 242 deletions(-) create mode 100644 app/models/concerns/integrations/base/telegram.rb create mode 100644 db/post_migrate/20250210064812_validate_approval_merge_request_rule_sources_project_id_not_null_constraint.rb create mode 100644 db/schema_migrations/20250210064812 create mode 100644 spec/models/integrations/instance/telegram_spec.rb create mode 100644 spec/support/shared_examples/models/concerns/integrations/base/telegram_shared_examples.rb diff --git a/app/assets/javascripts/performance_bar/components/add_request.vue b/app/assets/javascripts/performance_bar/components/add_request.vue index 646840e3803..2bc13d7bcf2 100644 --- a/app/assets/javascripts/performance_bar/components/add_request.vue +++ b/app/assets/javascripts/performance_bar/components/add_request.vue @@ -6,6 +6,7 @@ export default { i18n: { buttonLabel: __('Add request manually'), inputLabel: __('URL or request ID'), + submitLabel: __('Add'), }, components: { GlForm, @@ -38,7 +39,7 @@ export default { diff --git a/app/models/concerns/integrations/base/telegram.rb b/app/models/concerns/integrations/base/telegram.rb new file mode 100644 index 00000000000..c68cf19a79f --- /dev/null +++ b/app/models/concerns/integrations/base/telegram.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +module Integrations + module Base + module Telegram + extend ActiveSupport::Concern + + include HasAvatar + include Base::ChatNotification + + TELEGRAM_HOSTNAME = "%{hostname}/bot%{token}/sendMessage" + + class_methods do + def title + 'Telegram' + end + + def description + s_("TelegramIntegration|Send notifications about project events to Telegram.") + end + + def to_param + 'telegram' + end + + def help + build_help_page_url( + 'user/project/integrations/telegram.md', + s_("TelegramIntegration|Send notifications about project events to Telegram.") + ) + end + + def supported_events + super - ['deployment'] + end + end + + included do + field :hostname, + title: -> { _('Hostname') }, + section: Integrations::Base::Integration::SECTION_TYPE_CONNECTION, + help: -> { _('Custom hostname of the Telegram API. The default value is `https://api.telegram.org`.') }, + placeholder: 'https://api.telegram.org', + exposes_secrets: true, + required: false + + field :token, + title: -> { _('Token') }, + section: Integrations::Base::Integration::SECTION_TYPE_CONNECTION, + help: -> { s_('TelegramIntegration|Unique authentication token.') }, + non_empty_password_title: -> { s_('TelegramIntegration|New token') }, + non_empty_password_help: -> { s_('TelegramIntegration|Leave blank to use your current token.') }, + placeholder: '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', + description: -> { _('The Telegram bot token (for example, `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`).') }, + exposes_secrets: true, + is_secret: true, + required: true + + field :room, + title: -> { _('Channel identifier') }, + section: Integrations::Base::Integration::SECTION_TYPE_CONFIGURATION, + help: -> { + _("Unique identifier for the target chat or the username of the target channel " \ + "(in the format `@channelusername`).") + }, + placeholder: '@channelusername', + required: true + + field :thread, + title: -> { _('Message thread ID') }, + section: Integrations::Base::Integration::SECTION_TYPE_CONFIGURATION, + help: -> { _('Unique identifier for the target message thread (topic in a forum supergroup).') }, + placeholder: '123', + required: false + + field :notify_only_broken_pipelines, + title: -> { _('Notify only broken pipelines') }, + type: :checkbox, + section: Integrations::Base::Integration::SECTION_TYPE_CONFIGURATION, + description: -> { _('Send notifications for broken pipelines.') }, + help: -> { _('If selected, successful pipelines do not trigger a notification event.') } + + field :branches_to_be_notified, + type: :select, + section: Integrations::Base::Integration::SECTION_TYPE_CONFIGURATION, + title: -> { s_('Integrations|Branches for which notifications are to be sent') }, + description: -> { + _('Branches to send notifications for. Valid options are `all`, `default`, `protected`, ' \ + 'and `default_and_protected`. The default value is `default`.') + }, + choices: -> { branch_choices } + + with_options if: :activated? do + validates :token, :room, presence: true + validates :thread, numericality: { only_integer: true }, allow_blank: true + end + + before_validation :set_webhook + + private + + def set_webhook + hostname = self.hostname.presence || 'https://api.telegram.org' + self.webhook = format(TELEGRAM_HOSTNAME, hostname: hostname, token: token) if token.present? + end + + def notify(message, _opts) + body = { + text: message.summary, + chat_id: room, + message_thread_id: thread, + parse_mode: 'markdown' + }.compact_blank + + header = { 'Content-Type' => 'application/json' } + response = Gitlab::HTTP.post(webhook, headers: header, body: Gitlab::Json.dump(body)) + + # We're retrying the request with a different format to ensure accurate formatting and + # avoid receiving a 400 response due to invalid markdown. + if response.bad_request? + body.except!(:parse_mode) + response = Gitlab::HTTP.post(webhook, headers: header, body: Gitlab::Json.dump(body)) + end + + response if response.success? + end + + def custom_data(data) + super.merge(markdown: true) + end + end + end + end +end diff --git a/app/models/integrations/instance/telegram.rb b/app/models/integrations/instance/telegram.rb index 9bb923c7a89..13e9400e3c6 100644 --- a/app/models/integrations/instance/telegram.rb +++ b/app/models/integrations/instance/telegram.rb @@ -3,7 +3,7 @@ module Integrations module Instance class Telegram < Integration - # To be updated as part of https://gitlab.com/gitlab-org/gitlab/-/issues/474809 + include Integrations::Base::Telegram end end end diff --git a/app/models/integrations/telegram.rb b/app/models/integrations/telegram.rb index 8da4580aeee..372f367b77d 100644 --- a/app/models/integrations/telegram.rb +++ b/app/models/integrations/telegram.rb @@ -2,125 +2,6 @@ module Integrations class Telegram < Integration - include HasAvatar - include Base::ChatNotification - - TELEGRAM_HOSTNAME = "%{hostname}/bot%{token}/sendMessage" - - field :hostname, - title: -> { _('Hostname') }, - section: SECTION_TYPE_CONNECTION, - help: -> { _('Custom hostname of the Telegram API. The default value is `https://api.telegram.org`.') }, - placeholder: 'https://api.telegram.org', - exposes_secrets: true, - required: false - - field :token, - title: -> { _('Token') }, - section: SECTION_TYPE_CONNECTION, - help: -> { s_('TelegramIntegration|Unique authentication token.') }, - non_empty_password_title: -> { s_('TelegramIntegration|New token') }, - non_empty_password_help: -> { s_('TelegramIntegration|Leave blank to use your current token.') }, - placeholder: '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', - description: -> { _('The Telegram bot token (for example, `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`).') }, - exposes_secrets: true, - is_secret: true, - required: true - - field :room, - title: -> { _('Channel identifier') }, - section: SECTION_TYPE_CONFIGURATION, - help: -> { - _("Unique identifier for the target chat or the username of the target channel " \ - "(in the format `@channelusername`).") - }, - placeholder: '@channelusername', - required: true - - field :thread, - title: -> { _('Message thread ID') }, - section: SECTION_TYPE_CONFIGURATION, - help: -> { _('Unique identifier for the target message thread (topic in a forum supergroup).') }, - placeholder: '123', - required: false - - field :notify_only_broken_pipelines, - title: -> { _('Notify only broken pipelines') }, - type: :checkbox, - section: SECTION_TYPE_CONFIGURATION, - description: -> { _('Send notifications for broken pipelines.') }, - help: -> { _('If selected, successful pipelines do not trigger a notification event.') } - - field :branches_to_be_notified, - type: :select, - section: SECTION_TYPE_CONFIGURATION, - title: -> { s_('Integrations|Branches for which notifications are to be sent') }, - description: -> { - _('Branches to send notifications for. Valid options are `all`, `default`, `protected`, ' \ - 'and `default_and_protected`. The default value is `default`.') - }, - choices: -> { branch_choices } - - with_options if: :activated? do - validates :token, :room, presence: true - validates :thread, numericality: { only_integer: true }, allow_blank: true - end - - before_validation :set_webhook - - def self.title - 'Telegram' - end - - def self.description - s_("TelegramIntegration|Send notifications about project events to Telegram.") - end - - def self.to_param - 'telegram' - end - - def self.help - build_help_page_url( - 'user/project/integrations/telegram.md', - s_("TelegramIntegration|Send notifications about project events to Telegram.") - ) - end - - def self.supported_events - super - ['deployment'] - end - - private - - def set_webhook - hostname = self.hostname.presence || 'https://api.telegram.org' - self.webhook = format(TELEGRAM_HOSTNAME, hostname: hostname, token: token) if token.present? - end - - def notify(message, _opts) - body = { - text: message.summary, - chat_id: room, - message_thread_id: thread, - parse_mode: 'markdown' - }.compact_blank - - header = { 'Content-Type' => 'application/json' } - response = Gitlab::HTTP.post(webhook, headers: header, body: Gitlab::Json.dump(body)) - - # We're retrying the request with a different format to ensure accurate formatting and - # avoid receiving a 400 response due to invalid markdown. - if response.bad_request? - body.except!(:parse_mode) - response = Gitlab::HTTP.post(webhook, headers: header, body: Gitlab::Json.dump(body)) - end - - response if response.success? - end - - def custom_data(data) - super(data).merge(markdown: true) - end + include Integrations::Base::Telegram end end diff --git a/db/docs/approval_merge_request_rule_sources.yml b/db/docs/approval_merge_request_rule_sources.yml index cba5725be13..0f2b3229176 100644 --- a/db/docs/approval_merge_request_rule_sources.yml +++ b/db/docs/approval_merge_request_rule_sources.yml @@ -8,14 +8,6 @@ description: Keeps connection between merge request and project approval rule introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8497 milestone: '11.7' gitlab_schema: gitlab_main_cell -desired_sharding_key: - project_id: - references: projects - backfill_via: - parent: - foreign_key: approval_project_rule_id - table: approval_project_rules - sharding_key: project_id - belongs_to: approval_project_rule -desired_sharding_key_migration_job_name: BackfillApprovalMergeRequestRuleSourcesProjectId table_size: small +sharding_key: + project_id: projects diff --git a/db/post_migrate/20250210064812_validate_approval_merge_request_rule_sources_project_id_not_null_constraint.rb b/db/post_migrate/20250210064812_validate_approval_merge_request_rule_sources_project_id_not_null_constraint.rb new file mode 100644 index 00000000000..70f3837c33b --- /dev/null +++ b/db/post_migrate/20250210064812_validate_approval_merge_request_rule_sources_project_id_not_null_constraint.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ValidateApprovalMergeRequestRuleSourcesProjectIdNotNullConstraint < Gitlab::Database::Migration[2.2] + milestone '17.10' + + def up + validate_not_null_constraint :approval_merge_request_rule_sources, :project_id + end + + def down + # no-op + end +end diff --git a/db/schema_migrations/20250210064812 b/db/schema_migrations/20250210064812 new file mode 100644 index 00000000000..e34fb1501a0 --- /dev/null +++ b/db/schema_migrations/20250210064812 @@ -0,0 +1 @@ +f46d1d2ae51d974cb2fd139b82f87b72a3ef4ca666f36630012ffef297f38f38 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 294c8afc0cf..1b6a5c7ba1b 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -8534,7 +8534,8 @@ CREATE TABLE approval_merge_request_rule_sources ( id bigint NOT NULL, approval_merge_request_rule_id bigint NOT NULL, approval_project_rule_id bigint NOT NULL, - project_id bigint + project_id bigint, + CONSTRAINT check_f82666a937 CHECK ((project_id IS NOT NULL)) ); CREATE SEQUENCE approval_merge_request_rule_sources_id_seq @@ -27257,9 +27258,6 @@ ALTER TABLE project_relation_exports ALTER TABLE merge_request_blocks ADD CONSTRAINT check_f8034ca45e CHECK ((project_id IS NOT NULL)) NOT VALID; -ALTER TABLE approval_merge_request_rule_sources - ADD CONSTRAINT check_f82666a937 CHECK ((project_id IS NOT NULL)) NOT VALID; - ALTER TABLE projects ADD CONSTRAINT check_fa75869cb1 CHECK ((project_namespace_id IS NOT NULL)) NOT VALID; diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 591b981fe29..b043be31ac6 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -21,6 +21,12 @@ Sometimes, HAML page is enough to satisfy requirements. This statement is correc To better explain this, let's imagine the page that has one toggle, and toggling it sends an API request. This case does not involve any state we want to maintain, we send the request and switch the toggle. However, if we add one more toggle that should always be the opposite to the first one, we need a _state_: one toggle should be "aware" about the state of another one. When written in plain JavaScript, this logic usually involves listening to DOM event and reacting with modifying DOM. Cases like this are much easier to handle with Vue.js so we should create a Vue application here. +## How to add a Vue application to a page + +1. Create a new folder in `app/assets/javascripts` for your Vue application. +1. Add [page-specific JavaScript](performance.md#page-specific-javascript) to load your application. +1. You can use the [`initSimpleApp helper](#the-initsimpleapp-helper) to simplify [passing data from HAML to JS](#providing-data-from-haml-to-javascript). + ### What are some flags signaling that you might need Vue application? - when you need to define complex conditionals based on multiple factors and update them on user interaction; diff --git a/spec/frontend/performance_bar/components/add_request_spec.js b/spec/frontend/performance_bar/components/add_request_spec.js index de9cc1e8008..846f53046de 100644 --- a/spec/frontend/performance_bar/components/add_request_spec.js +++ b/spec/frontend/performance_bar/components/add_request_spec.js @@ -8,6 +8,7 @@ describe('add request form', () => { const findGlFormInput = () => wrapper.findComponent(GlFormInput); const findGlButton = () => wrapper.findComponent(GlButton); + const findGlSubmit = () => wrapper.findComponent('[type=submit]'); beforeEach(() => { wrapper = mount(AddRequest); @@ -15,6 +16,7 @@ describe('add request form', () => { it('hides the input on load', () => { expect(findGlFormInput().exists()).toBe(false); + expect(findGlSubmit().exists()).toBe(false); }); describe('when clicking the button', () => { @@ -25,6 +27,7 @@ describe('add request form', () => { it('shows the form', () => { expect(findGlFormInput().exists()).toBe(true); + expect(findGlSubmit().exists()).toBe(true); }); describe('when pressing escape', () => { @@ -35,6 +38,7 @@ describe('add request form', () => { it('hides the input', () => { expect(findGlFormInput().exists()).toBe(false); + expect(findGlSubmit().exists()).toBe(false); }); }); @@ -42,7 +46,7 @@ describe('add request form', () => { beforeEach(async () => { findGlFormInput().setValue('http://gitlab.example.com/users/root/calendar.json'); await nextTick(); - findGlFormInput().trigger('keyup.enter'); + findGlSubmit().trigger('submit'); await nextTick(); }); @@ -55,6 +59,7 @@ describe('add request form', () => { it('hides the input', () => { expect(findGlFormInput().exists()).toBe(false); + expect(findGlSubmit().exists()).toBe(false); }); it('clears the value for next time', async () => { diff --git a/spec/models/integrations/instance/telegram_spec.rb b/spec/models/integrations/instance/telegram_spec.rb new file mode 100644 index 00000000000..4e757f4bf5b --- /dev/null +++ b/spec/models/integrations/instance/telegram_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Integrations::Instance::Telegram, feature_category: :integrations do + it_behaves_like Integrations::Base::Telegram +end diff --git a/spec/models/integrations/telegram_spec.rb b/spec/models/integrations/telegram_spec.rb index 000cac7ad82..20592b8b39b 100644 --- a/spec/models/integrations/telegram_spec.rb +++ b/spec/models/integrations/telegram_spec.rb @@ -3,99 +3,5 @@ require "spec_helper" RSpec.describe Integrations::Telegram, feature_category: :integrations do - it_behaves_like Integrations::HasAvatar - it_behaves_like "chat integration", "Telegram" do - let(:payload) do - { - text: be_present - } - end - end - - describe 'validations' do - context 'when integration is active' do - before do - subject.activate! - end - - it { is_expected.to validate_presence_of(:token) } - it { is_expected.to validate_presence_of(:room) } - it { is_expected.to validate_numericality_of(:thread).only_integer } - end - - context 'when integration is inactive' do - before do - subject.deactivate! - end - - it { is_expected.not_to validate_presence_of(:token) } - it { is_expected.not_to validate_presence_of(:room) } - it { is_expected.not_to validate_numericality_of(:thread).only_integer } - end - end - - describe 'before_validation :set_webhook' do - context 'when token is not present' do - let(:integration) { build(:telegram_integration, token: nil) } - - it 'does not set webhook value' do - expect(integration.webhook).to eq(nil) - expect(integration).not_to be_valid - end - end - - context 'when token is present' do - let(:integration) { build_stubbed(:telegram_integration) } - - it 'sets webhook value' do - expect(integration).to be_valid - expect(integration.webhook).to eq("https://api.telegram.org/bot123456:ABC-DEF1234/sendMessage") - end - - context 'with custom hostname' do - before do - integration.hostname = 'https://gitlab.example.com' - end - - it 'sets webhook value with custom hostname' do - expect(integration).to be_valid - expect(integration.webhook).to eq("https://gitlab.example.com/bot123456:ABC-DEF1234/sendMessage") - end - end - end - end - - describe '#notify' do - let(:subject) { build(:telegram_integration) } - let(:message) { instance_double(Integrations::ChatMessage::PushMessage, summary: '_Test message') } - let(:header) { { 'Content-Type' => 'application/json' } } - let(:response) { instance_double(HTTParty::Response, bad_request?: true, success?: true) } - let(:body_1) do - { - text: '_Test message', - chat_id: subject.room, - message_thread_id: subject.thread, - parse_mode: 'markdown' - }.compact_blank - end - - let(:body_2) { body_1.without(:parse_mode) } - - before do - allow(Gitlab::HTTP).to receive(:post).and_return(response) - end - - it 'removes the parse mode if the first request fails with a bad request' do - expect(Gitlab::HTTP).to receive(:post).with(subject.webhook, headers: header, body: Gitlab::Json.dump(body_1)) - expect(Gitlab::HTTP).to receive(:post).with(subject.webhook, headers: header, body: Gitlab::Json.dump(body_2)) - - subject.send(:notify, message, {}) - end - - it 'makes a second request if the first one fails with a bad request' do - expect(Gitlab::HTTP).to receive(:post).twice - - subject.send(:notify, message, {}) - end - end + it_behaves_like Integrations::Base::Telegram end diff --git a/spec/support/shared_examples/models/concerns/integrations/base/telegram_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/base/telegram_shared_examples.rb new file mode 100644 index 00000000000..270fb159dab --- /dev/null +++ b/spec/support/shared_examples/models/concerns/integrations/base/telegram_shared_examples.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +RSpec.shared_examples Integrations::Base::Telegram do + it_behaves_like Integrations::HasAvatar + it_behaves_like "chat integration", "Telegram" do + let(:payload) do + { + text: be_present + } + end + end + + describe 'validations' do + subject(:integration) { build(:telegram_integration) } + + context 'when integration is active' do + before do + integration.activate! + end + + it { is_expected.to validate_presence_of(:token) } + it { is_expected.to validate_presence_of(:room) } + it { is_expected.to validate_numericality_of(:thread).only_integer } + end + + context 'when integration is inactive' do + before do + integration.deactivate! + end + + it { is_expected.not_to validate_presence_of(:token) } + it { is_expected.not_to validate_presence_of(:room) } + it { is_expected.not_to validate_numericality_of(:thread).only_integer } + end + end + + describe 'before_validation :set_webhook' do + context 'when token is not present' do + subject(:integration) { build(:telegram_integration, token: nil) } + + it 'does not set webhook value' do + expect(integration.webhook).to be_nil + expect(integration).not_to be_valid + end + end + + context 'when token is present' do + subject(:integration) { build_stubbed(:telegram_integration) } + + it 'sets webhook value' do + expect(integration).to be_valid + expect(integration.webhook).to eq("https://api.telegram.org/bot123456:ABC-DEF1234/sendMessage") + end + + context 'with custom hostname' do + before do + integration.hostname = 'https://gitlab.example.com' + end + + it 'sets webhook value with custom hostname' do + expect(integration).to be_valid + expect(integration.webhook).to eq("https://gitlab.example.com/bot123456:ABC-DEF1234/sendMessage") + end + end + end + end + + describe '#notify' do + let(:message) { instance_double(Integrations::ChatMessage::PushMessage, summary: '_Test message') } + let(:header) { { 'Content-Type' => 'application/json' } } + let(:response) { instance_double(HTTParty::Response, bad_request?: true, success?: true) } + let(:body_1) do + { + text: '_Test message', + chat_id: integration.room, + message_thread_id: integration.thread, + parse_mode: 'markdown' + }.compact_blank + end + + let(:body_2) { body_1.without(:parse_mode) } + + subject(:integration) { build(:telegram_integration) } + + before do + allow(Gitlab::HTTP).to receive(:post).and_return(response) + end + + it 'removes the parse mode if the first request fails with a bad request' do + expect(Gitlab::HTTP).to receive(:post).with(integration.webhook, headers: header, body: Gitlab::Json.dump(body_1)) + expect(Gitlab::HTTP).to receive(:post).with(integration.webhook, headers: header, body: Gitlab::Json.dump(body_2)) + + integration.send(:notify, message, {}) + end + + it 'makes a second request if the first one fails with a bad request' do + expect(Gitlab::HTTP).to receive(:post).twice + + integration.send(:notify, message, {}) + end + end +end