diff --git a/app/models/concerns/integrations/has_data_fields.rb b/app/models/concerns/integrations/has_data_fields.rb
index 25a1d8551197e5af6c70d0f046a2051d3918677a..635147a2f3ce9f9cc1eb5f791d642e7592d47f46 100644
--- a/app/models/concerns/integrations/has_data_fields.rb
+++ b/app/models/concerns/integrations/has_data_fields.rb
@@ -12,7 +12,8 @@ def data_field(*args)
           self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
             unless method_defined?(arg)
               def #{arg}
-                data_fields.send('#{arg}') || (properties && properties['#{arg}'])
+                value = data_fields.send('#{arg}')
+                value.nil? ? properties&.dig('#{arg}') : value
               end
             end
 
diff --git a/spec/models/concerns/integrations/has_data_fields_spec.rb b/spec/models/concerns/integrations/has_data_fields_spec.rb
index b28fef571c65fccf34c01e9b115c2be87dbcc544..374c5c33b5027ffb25bc53a3a3fe7e9acbc2793e 100644
--- a/spec/models/concerns/integrations/has_data_fields_spec.rb
+++ b/spec/models/concerns/integrations/has_data_fields_spec.rb
@@ -6,84 +6,84 @@
   let(:url) { 'http://url.com' }
   let(:username) { 'username_one' }
   let(:properties) do
-    { url: url, username: username }
+    { url: url, username: username, jira_issue_transition_automatic: false }
   end
 
   shared_examples 'data fields' do
     describe '#arg' do
-      it 'returns an argument correctly' do
-        expect(service.url).to eq(url)
+      it 'returns the expected values' do
+        expect(integration).to have_attributes(properties)
       end
     end
 
     describe '{arg}_changed?' do
       it 'returns false when the property has not been assigned a new value' do
-        service.username = 'new_username'
-        service.validate
-        expect(service.url_changed?).to be_falsy
+        integration.username = 'new_username'
+        integration.validate
+        expect(integration.url_changed?).to be_falsy
       end
 
       it 'returns true when the property has been assigned a different value' do
-        service.url = "http://example.com"
-        service.validate
-        expect(service.url_changed?).to be_truthy
+        integration.url = "http://example.com"
+        integration.validate
+        expect(integration.url_changed?).to be_truthy
       end
 
       it 'returns true when the property has been assigned a different value twice' do
-        service.url = "http://example.com"
-        service.url = "http://example.com"
-        service.validate
-        expect(service.url_changed?).to be_truthy
+        integration.url = "http://example.com"
+        integration.url = "http://example.com"
+        integration.validate
+        expect(integration.url_changed?).to be_truthy
       end
 
       it 'returns false when the property has been re-assigned the same value' do
-        service.url = 'http://url.com'
-        service.validate
-        expect(service.url_changed?).to be_falsy
+        integration.url = 'http://url.com'
+        integration.validate
+        expect(integration.url_changed?).to be_falsy
       end
     end
 
     describe '{arg}_touched?' do
       it 'returns false when the property has not been assigned a new value' do
-        service.username = 'new_username'
-        service.validate
-        expect(service.url_changed?).to be_falsy
+        integration.username = 'new_username'
+        integration.validate
+        expect(integration.url_changed?).to be_falsy
       end
 
       it 'returns true when the property has been assigned a different value' do
-        service.url = "http://example.com"
-        service.validate
-        expect(service.url_changed?).to be_truthy
+        integration.url = "http://example.com"
+        integration.validate
+        expect(integration.url_changed?).to be_truthy
       end
 
       it 'returns true when the property has been assigned a different value twice' do
-        service.url = "http://example.com"
-        service.url = "http://example.com"
-        service.validate
-        expect(service.url_changed?).to be_truthy
+        integration.url = "http://example.com"
+        integration.url = "http://example.com"
+        integration.validate
+        expect(integration.url_changed?).to be_truthy
       end
 
       it 'returns true when the property has been re-assigned the same value' do
-        service.url = 'http://url.com'
-        expect(service.url_touched?).to be_truthy
+        integration.url = 'http://url.com'
+        expect(integration.url_touched?).to be_truthy
       end
 
       it 'returns false when the property has been re-assigned the same value' do
-        service.url = 'http://url.com'
-        service.validate
-        expect(service.url_changed?).to be_falsy
+        integration.url = 'http://url.com'
+        integration.validate
+        expect(integration.url_changed?).to be_falsy
       end
     end
 
     describe 'data_fields_present?' do
-      it 'returns true from the issue tracker service' do
-        expect(service.data_fields_present?).to be true
+      it 'returns true from the issue tracker integration' do
+        expect(integration.data_fields_present?).to be true
       end
     end
   end
 
   context 'when data are stored in data_fields' do
-    let(:service) do
+    let(:integration) do
       create(:jira_integration, url: url, username: username)
     end
 
@@ -91,21 +91,21 @@
 
     describe '{arg}_was?' do
       it 'returns nil' do
-        service.url = 'http://example.com'
-        service.validate
-        expect(service.url_was).to be_nil
+        integration.url = 'http://example.com'
+        integration.validate
+        expect(integration.url_was).to be_nil
       end
     end
   end
 
-  context 'when service and data_fields are not persisted' do
-    let(:service) do
+  context 'when integration and data_fields are not persisted' do
+    let(:integration) do
       Integrations::Jira.new
     end
 
     describe 'data_fields_present?' do
       it 'returns true' do
-        expect(service.data_fields_present?).to be true
+        expect(integration.data_fields_present?).to be true
       end
     end
   end
@@ -113,9 +113,7 @@
   context 'when data are stored in properties' do
     let(:integration) { create(:jira_integration, :without_properties_callback, properties: properties) }
 
-    it_behaves_like 'data fields' do
-      let(:service) { integration }
-    end
+    it_behaves_like 'data fields'
 
     describe '{arg}_was?' do
       it 'returns nil when the property has not been assigned a new value' do
@@ -148,9 +146,7 @@
       end
     end
 
-    it_behaves_like 'data fields' do
-      let(:service) { integration }
-    end
+    it_behaves_like 'data fields'
 
     describe '{arg}_was?' do
       it 'returns nil' do
diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb
index 96cc101e73a982a1687b0fd5b27856f509b46ecf..cd9a074658185eaa58ef67c632226d9c21600bd4 100644
--- a/spec/requests/api/integrations_spec.rb
+++ b/spec/requests/api/integrations_spec.rb
@@ -55,33 +55,49 @@ def self.integration_names
       describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do
         include_context integration
 
+        # NOTE: Some attributes are not supported for PUT requests, even though in most cases they should be.
+        # For some of them the problem is somewhere else, i.e. most chat integrations don't support the `*_channel`
+        # fields but they're incorrectly included in `#fields`.
+        #
+        # We can fix these manually, or with a generic approach like https://gitlab.com/gitlab-org/gitlab/-/issues/348208
+        let(:missing_channel_attributes) { %i[push_channel issue_channel confidential_issue_channel merge_request_channel note_channel confidential_note_channel tag_push_channel pipeline_channel wiki_page_channel] }
+        let(:missing_attributes) do
+          {
+            datadog: %i[archive_trace_events],
+            discord: missing_channel_attributes + %i[branches_to_be_notified notify_only_broken_pipelines],
+            hangouts_chat: missing_channel_attributes + %i[notify_only_broken_pipelines],
+            jira: %i[issues_enabled project_key vulnerabilities_enabled vulnerabilities_issuetype],
+            mattermost: %i[deployment_channel labels_to_be_notified],
+            microsoft_teams: missing_channel_attributes,
+            mock_ci: %i[enable_ssl_verification],
+            prometheus: %i[manual_configuration],
+            slack: %i[alert_events alert_channel deployment_channel labels_to_be_notified],
+            unify_circuit: missing_channel_attributes + %i[branches_to_be_notified notify_only_broken_pipelines],
+            webex_teams: missing_channel_attributes + %i[branches_to_be_notified notify_only_broken_pipelines]
+          }
+        end
+
         it "updates #{integration} settings and returns the correct fields" do
-          put api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user), params: integration_attrs
+          supported_attrs = integration_attrs.without(missing_attributes.fetch(integration.to_sym, []))
+
+          put api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user), params: supported_attrs
 
           expect(response).to have_gitlab_http_status(:ok)
+          expect(json_response['slug']).to eq(dashed_integration)
 
           current_integration = project.integrations.first
-          events = current_integration.event_names.empty? ? ["foo"].freeze : current_integration.event_names
-          query_strings = []
-          events.map(&:to_sym).each do |event|
-            event_value = !current_integration[event]
-            query_strings << "#{event}=#{event_value}"
-            integration_attrs[event] = event_value if integration_attrs[event].present?
+          expect(current_integration).to have_attributes(supported_attrs)
+          assert_correct_response_fields(json_response['properties'].keys, current_integration)
+
+          # Flip all booleans and verify that we can set these too
+          flipped_attrs = supported_attrs.transform_values do |value|
+            [true, false].include?(value) ? !value : value
           end
-          query_strings = query_strings.join('&')
 
-          put api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}?#{query_strings}", user), params: integration_attrs
+          put api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user), params: flipped_attrs
 
           expect(response).to have_gitlab_http_status(:ok)
-          expect(json_response['slug']).to eq(dashed_integration)
-          events.each do |event|
-            next if event == "foo"
-
-            expect(project.integrations.first[event]).not_to eq(current_integration[event]),
-                                                             "expected #{!current_integration[event]} for event #{event} for #{endpoint} #{current_integration.title}, got #{current_integration[event]}"
-          end
-
-          assert_correct_response_fields(json_response['properties'].keys, current_integration)
+          expect(project.integrations.first).to have_attributes(flipped_attrs)
         end
 
         it "returns if required fields missing" do
diff --git a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
index a644e5d5b9c3b53d3f892cfdd0f6d268f3b421a1..3d3b8c2207d438e24951550670e2818f3c63b7f9 100644
--- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
@@ -8,8 +8,36 @@
     let(:integration_method) { Project.integration_association_name(integration) }
     let(:integration_klass) { Integration.integration_name_to_model(integration) }
     let(:integration_instance) { integration_klass.new }
-    let(:integration_fields) { integration_instance.fields }
-    let(:integration_attrs_list) { integration_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } }
+
+    # Build a list of all attributes that an integration supports.
+    let(:integration_attrs_list) do
+      integration_fields + integration_events + custom_attributes.fetch(integration.to_sym, [])
+    end
+
+    # Attributes defined as fields.
+    let(:integration_fields) do
+      integration_instance.fields.map { _1[:name].to_sym }
+    end
+
+    # Attributes for configurable event triggers.
+    let(:integration_events) do
+      integration_instance.configurable_events.map { IntegrationsHelper.integration_event_field_name(_1).to_sym }
+    end
+
+    # Other special cases, this list might be incomplete.
+    #
+    # Some of these won't be needed anymore after we've converted them to use the field DSL
+    # in https://gitlab.com/gitlab-org/gitlab/-/issues/354899.
+    #
+    # Others like `comment_on_event_disabled` are actual columns on `integrations`, maybe we should migrate
+    # these to fields as well.
+    let(:custom_attributes) do
+      {
+        jira: %i[comment_on_event_enabled jira_issue_transition_automatic jira_issue_transition_id project_key
+                 issues_enabled vulnerabilities_enabled vulnerabilities_issuetype]
+      }
+    end
+
     let(:integration_attrs) do
       integration_attrs_list.inject({}) do |hash, k|
         if k =~ /^(token*|.*_token|.*_key)/
@@ -32,9 +60,11 @@
           hash.merge!(k => 1234)
         elsif integration == 'jira' && k == :jira_issue_transition_id
           hash.merge!(k => '1,2,3')
+        elsif integration == 'jira' && k == :jira_issue_transition_automatic
+          hash.merge!(k => true)
         elsif integration == 'emails_on_push' && k == :recipients
           hash.merge!(k => 'foo@bar.com')
-        elsif integration == 'slack' || integration == 'mattermost' && k == :labels_to_be_notified_behavior
+        elsif (integration == 'slack' || integration == 'mattermost') && k == :labels_to_be_notified_behavior
           hash.merge!(k => "match_any")
         else
           hash.merge!(k => "someword")