From b57142452211f98175ac75f473741c67988867f5 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 19 Nov 2020 03:08:59 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .rubocop_manual_todo.yml | 1 - Gemfile | 2 +- Gemfile.lock | 6 +- app/graphql/mutations/issues/update.rb | 2 +- .../issuable/import_csv/base_service.rb | 8 +- ...246857-fix-issuable-import-csv-service.yml | 5 + doc/api/features.md | 74 +++++++- lib/api/entities/feature.rb | 10 ++ lib/api/features.rb | 12 +- lib/feature.rb | 2 - lib/feature/definition.rb | 14 +- spec/fixtures/csv_empty.csv | 0 .../lib/banzai/filter/markdown_filter_spec.rb | 6 + spec/requests/api/features_spec.rb | 166 ++++++++++++------ ...able_import_csv_service_shared_examples.rb | 32 ++-- 15 files changed, 248 insertions(+), 92 deletions(-) create mode 100644 changelogs/unreleased/246857-fix-issuable-import-csv-service.yml create mode 100644 spec/fixtures/csv_empty.csv diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index b266435d4d2..b96cc49e01d 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -26,7 +26,6 @@ Graphql/IDType: - 'ee/app/graphql/mutations/iterations/update.rb' - 'ee/app/graphql/resolvers/iterations_resolver.rb' - 'app/graphql/mutations/boards/issues/issue_move_list.rb' - - 'app/graphql/mutations/issues/update.rb' - 'app/graphql/mutations/metrics/dashboard/annotations/delete.rb' - 'app/graphql/resolvers/design_management/design_at_version_resolver.rb' - 'app/graphql/resolvers/design_management/design_resolver.rb' diff --git a/Gemfile b/Gemfile index d60fefe29dc..82a09e2f7eb 100644 --- a/Gemfile +++ b/Gemfile @@ -149,7 +149,7 @@ gem 'html-pipeline', '~> 2.12' gem 'deckar01-task_list', '2.3.1' gem 'gitlab-markup', '~> 1.7.1' gem 'github-markup', '~> 1.7.0', require: 'github/markup' -gem 'commonmarker', '~> 0.20' +gem 'commonmarker', '~> 0.21' gem 'kramdown', '~> 2.3.0' gem 'RedCloth', '~> 4.3.2' gem 'rdoc', '~> 6.1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 64179847dd8..807c24c0d31 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -177,7 +177,7 @@ GEM open4 (~> 1.3) coderay (1.1.3) colored2 (3.1.2) - commonmarker (0.20.1) + commonmarker (0.21.0) ruby-enum (~> 0.5) concord (0.1.5) adamantium (~> 0.2.0) @@ -1041,7 +1041,7 @@ GEM rubocop-rspec (1.44.1) rubocop (~> 0.87) rubocop-ast (>= 0.7.1) - ruby-enum (0.7.2) + ruby-enum (0.8.0) i18n ruby-fogbugz (0.2.1) crack (~> 0.4) @@ -1292,7 +1292,7 @@ DEPENDENCIES capybara-screenshot (~> 1.0.22) carrierwave (~> 1.3) charlock_holmes (~> 0.7.7) - commonmarker (~> 0.20) + commonmarker (~> 0.21) concurrent-ruby (~> 1.1) connection_pool (~> 2.0) countries (~> 3.0) diff --git a/app/graphql/mutations/issues/update.rb b/app/graphql/mutations/issues/update.rb index 9b216b31f9b..d34e351b2a6 100644 --- a/app/graphql/mutations/issues/update.rb +++ b/app/graphql/mutations/issues/update.rb @@ -11,7 +11,7 @@ module Mutations required: false, description: copy_field_description(Types::IssueType, :title) - argument :milestone_id, GraphQL::ID_TYPE, + argument :milestone_id, GraphQL::ID_TYPE, # rubocop: disable Graphql/IDType required: false, description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null' diff --git a/app/services/issuable/import_csv/base_service.rb b/app/services/issuable/import_csv/base_service.rb index bf5f643a51b..77a096e7586 100644 --- a/app/services/issuable/import_csv/base_service.rb +++ b/app/services/issuable/import_csv/base_service.rb @@ -38,7 +38,7 @@ module Issuable def with_csv_lines csv_data = @csv_io.open(&:read).force_encoding(Encoding::UTF_8) - verify_headers!(csv_data) + validate_headers_presence!(csv_data.lines.first) csv_parsing_params = { col_sep: detect_col_sep(csv_data.lines.first), @@ -49,9 +49,9 @@ module Issuable CSV.new(csv_data, csv_parsing_params).each.with_index(2) end - def verify_headers!(data) - headers = data.lines.first.downcase - return if headers.include?('title') && headers.include?('description') + def validate_headers_presence!(headers) + headers.downcase! if headers + return if headers && headers.include?('title') && headers.include?('description') raise CSV::MalformedCSVError end diff --git a/changelogs/unreleased/246857-fix-issuable-import-csv-service.yml b/changelogs/unreleased/246857-fix-issuable-import-csv-service.yml new file mode 100644 index 00000000000..4e3bbfa49bc --- /dev/null +++ b/changelogs/unreleased/246857-fix-issuable-import-csv-service.yml @@ -0,0 +1,5 @@ +--- +title: Fix error in Issuable::ImportCsv::BaseService when CSV file is empty +merge_request: 47918 +author: +type: fixed diff --git a/doc/api/features.md b/doc/api/features.md index bbf86eca490..f94f6b36ce2 100644 --- a/doc/api/features.md +++ b/doc/api/features.md @@ -37,7 +37,8 @@ Example response: "key": "boolean", "value": false } - ] + ], + "definition": null }, { "name": "my_user_feature", @@ -47,7 +48,15 @@ Example response: "key": "percentage_of_actors", "value": 34 } - ] + ], + "definition": { + "name": "my_user_feature", + "introduced_by_url": "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40880", + "rollout_issue_url": "https://gitlab.com/gitlab-org/gitlab/-/issues/244905", + "group": "group::ci", + "type": "development", + "default_enabled": false + } }, { "name": "new_library", @@ -57,7 +66,45 @@ Example response: "key": "boolean", "value": true } - ] + ], + "definition": null + } +] +``` + +## List all feature definitions + +Get a list of all feature definitions. + +```plaintext +GET /features/definitions +``` + +```shell +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/features/definitions" +``` + +Example response: + +```json +[ + { + "name": "api_kaminari_count_with_limit", + "introduced_by_url": "https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23931", + "rollout_issue_url": null, + "milestone": "11.8", + "type": "ops", + "group": "group::ecosystem", + "default_enabled": false + }, + { + "name": "marginalia", + "introduced_by_url": null, + "rollout_issue_url": null, + "milestone": null, + "type": "ops", + "group": null, + "default_enabled": false } ] ``` @@ -81,6 +128,7 @@ POST /features/:name | `user` | string | no | A GitLab username | | `group` | string | no | A GitLab group's path, for example `gitlab-org` | | `project` | string | no | A projects path, for example `gitlab-org/gitlab-foss` | +| `force` | boolean | no | Skip feature flag validation checks, ie. YAML definition | Note that you can enable or disable a feature for a `feature_group`, a `user`, a `group`, and a `project` in a single API call. @@ -104,7 +152,15 @@ Example response: "key": "percentage_of_time", "value": 30 } - ] + ], + "definition": { + "name": "my_user_feature", + "introduced_by_url": "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40880", + "rollout_issue_url": "https://gitlab.com/gitlab-org/gitlab/-/issues/244905", + "group": "group::ci", + "type": "development", + "default_enabled": false + } } ``` @@ -133,7 +189,15 @@ Example response: "key": "percentage_of_actors", "value": 42 } - ] + ], + "definition": { + "name": "my_user_feature", + "introduced_by_url": "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40880", + "rollout_issue_url": "https://gitlab.com/gitlab-org/gitlab/-/issues/244905", + "group": "group::ci", + "type": "development", + "default_enabled": false + } } ``` diff --git a/lib/api/entities/feature.rb b/lib/api/entities/feature.rb index 618a7be9c7b..d1151849cd7 100644 --- a/lib/api/entities/feature.rb +++ b/lib/api/entities/feature.rb @@ -17,6 +17,16 @@ module API { key: gate.key, value: value } end.compact end + + class Definition < Grape::Entity + ::Feature::Definition::PARAMS.each do |param| + expose param + end + end + + expose :definition, using: Definition do |feature| + ::Feature::Definition.definitions[feature.name.to_sym] + end end end end diff --git a/lib/api/features.rb b/lib/api/features.rb index 2c2e3e3d0c9..2ab60e2617d 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -46,6 +46,15 @@ module API present features, with: Entities::Feature, current_user: current_user end + desc 'Get a list of all feature definitions' do + success Entities::Feature::Definition + end + get :definitions do + definitions = ::Feature::Definition.definitions.values.map(&:to_h) + + present definitions, with: Entities::Feature::Definition, current_user: current_user + end + desc 'Set the gate value for the given feature' do success Entities::Feature end @@ -56,6 +65,7 @@ module API optional :user, type: String, desc: 'A GitLab username' optional :group, type: String, desc: "A GitLab group's path, such as 'gitlab-org'" optional :project, type: String, desc: 'A projects path, like gitlab-org/gitlab-ce' + optional :force, type: Boolean, desc: 'Skip feature flag validation checks, ie. YAML definition' mutually_exclusive :key, :feature_group mutually_exclusive :key, :user @@ -63,7 +73,7 @@ module API mutually_exclusive :key, :project end post ':name' do - validate_feature_flag_name!(params[:name]) + validate_feature_flag_name!(params[:name]) unless params[:force] feature = Feature.get(params[:name]) # rubocop:disable Gitlab/AvoidFeatureGet targets = gate_targets(params) diff --git a/lib/feature.rb b/lib/feature.rb index 1f8c530bee5..c9871881dc9 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -136,8 +136,6 @@ class Feature end def register_definitions - return unless check_feature_flags_definition? - Feature::Definition.reload! end diff --git a/lib/feature/definition.rb b/lib/feature/definition.rb index 0ba1bdc4799..24f4cb3dbf0 100644 --- a/lib/feature/definition.rb +++ b/lib/feature/definition.rb @@ -13,6 +13,12 @@ class Feature end end + TYPES.each do |type, _| + define_method("#{type}?") do + attributes[:type].to_sym == type + end + end + def initialize(path, opts = {}) @path = path @attributes = {} @@ -94,6 +100,10 @@ class Feature @definitions = load_all! end + def has_definition?(key) + definitions.has_key?(key.to_sym) + end + def valid_usage!(key, type:, default_enabled:) if definition = definitions[key.to_sym] definition.valid_usage!(type_in_code: type, default_enabled_in_code: default_enabled) @@ -119,10 +129,6 @@ class Feature private def load_all! - # We currently do not load feature flag definitions - # in production environments - return [] unless Gitlab.dev_or_test_env? - paths.each_with_object({}) do |glob_path, definitions| load_all_from_path!(definitions, glob_path) end diff --git a/spec/fixtures/csv_empty.csv b/spec/fixtures/csv_empty.csv new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb index 8d01a651651..c5e84a0c1e7 100644 --- a/spec/lib/banzai/filter/markdown_filter_spec.rb +++ b/spec/lib/banzai/filter/markdown_filter_spec.rb @@ -46,6 +46,12 @@ RSpec.describe Banzai::Filter::MarkdownFilter do expect(result).to start_with('
')
       end
+
+      it 'works with additional language parameters' do
+        result = filter("```ruby:red gem\nsome code\n```", no_sourcepos: true)
+
+        expect(result).to start_with('
')
+      end
     end
   end
 
diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb
index 3f443b4f92b..acc49768545 100644
--- a/spec/requests/api/features_spec.rb
+++ b/spec/requests/api/features_spec.rb
@@ -6,6 +6,18 @@ RSpec.describe API::Features, stub_feature_flags: false do
   let_it_be(:user)  { create(:user) }
   let_it_be(:admin) { create(:admin) }
 
+  # Find any `development` feature flag name
+  let(:known_feature_flag) do
+    Feature::Definition.definitions
+      .values.find(&:development?)
+  end
+
+  let(:known_feature_flag_definition_hash) do
+    a_hash_including(
+      'type' => 'development'
+    )
+  end
+
   before do
     Feature.reset
     Flipper.unregister_groups
@@ -22,12 +34,14 @@ RSpec.describe API::Features, stub_feature_flags: false do
         {
           'name' => 'feature_1',
           'state' => 'on',
-          'gates' => [{ 'key' => 'boolean', 'value' => true }]
+          'gates' => [{ 'key' => 'boolean', 'value' => true }],
+          'definition' => nil
         },
         {
           'name' => 'feature_2',
           'state' => 'off',
-          'gates' => [{ 'key' => 'boolean', 'value' => false }]
+          'gates' => [{ 'key' => 'boolean', 'value' => false }],
+          'definition' => nil
         },
         {
           'name' => 'feature_3',
@@ -35,7 +49,14 @@ RSpec.describe API::Features, stub_feature_flags: false do
           'gates' => [
             { 'key' => 'boolean', 'value' => false },
             { 'key' => 'groups', 'value' => ['perf_team'] }
-          ]
+          ],
+          'definition' => nil
+        },
+        {
+          'name' => known_feature_flag.name,
+          'state' => 'on',
+          'gates' => [{ 'key' => 'boolean', 'value' => true }],
+          'definition' => known_feature_flag_definition_hash
         }
       ]
     end
@@ -44,6 +65,7 @@ RSpec.describe API::Features, stub_feature_flags: false do
       Feature.enable('feature_1')
       Feature.disable('feature_2')
       Feature.enable('feature_3', Feature.group(:perf_team))
+      Feature.enable(known_feature_flag.name)
     end
 
     it 'returns a 401 for anonymous users' do
@@ -67,7 +89,7 @@ RSpec.describe API::Features, stub_feature_flags: false do
   end
 
   describe 'POST /feature' do
-    let(:feature_name) { 'my_feature' }
+    let(:feature_name) { known_feature_flag.name }
 
     context 'when the feature does not exist' do
       it 'returns a 401 for anonymous users' do
@@ -87,43 +109,49 @@ RSpec.describe API::Features, stub_feature_flags: false do
           post api("/features/#{feature_name}", admin), params: { value: 'true' }
 
           expect(response).to have_gitlab_http_status(:created)
-          expect(json_response).to eq(
-            'name' => 'my_feature',
+          expect(json_response).to match(
+            'name' => feature_name,
             'state' => 'on',
-            'gates' => [{ 'key' => 'boolean', 'value' => true }])
+            'gates' => [{ 'key' => 'boolean', 'value' => true }],
+            'definition' => known_feature_flag_definition_hash
+          )
         end
 
         it 'creates an enabled feature for the given Flipper group when passed feature_group=perf_team' do
           post api("/features/#{feature_name}", admin), params: { value: 'true', feature_group: 'perf_team' }
 
           expect(response).to have_gitlab_http_status(:created)
-          expect(json_response).to eq(
-            'name' => 'my_feature',
+          expect(json_response).to match(
+            'name' => feature_name,
             'state' => 'conditional',
             'gates' => [
               { 'key' => 'boolean', 'value' => false },
               { 'key' => 'groups', 'value' => ['perf_team'] }
-            ])
+            ],
+            'definition' => known_feature_flag_definition_hash
+          )
         end
 
         it 'creates an enabled feature for the given user when passed user=username' do
           post api("/features/#{feature_name}", admin), params: { value: 'true', user: user.username }
 
           expect(response).to have_gitlab_http_status(:created)
-          expect(json_response).to eq(
-            'name' => 'my_feature',
+          expect(json_response).to match(
+            'name' => feature_name,
             'state' => 'conditional',
             'gates' => [
               { 'key' => 'boolean', 'value' => false },
               { 'key' => 'actors', 'value' => ["User:#{user.id}"] }
-            ])
+            ],
+            'definition' => known_feature_flag_definition_hash
+          )
         end
 
         it 'creates an enabled feature for the given user and feature group when passed user=username and feature_group=perf_team' do
           post api("/features/#{feature_name}", admin), params: { value: 'true', user: user.username, feature_group: 'perf_team' }
 
           expect(response).to have_gitlab_http_status(:created)
-          expect(json_response['name']).to eq('my_feature')
+          expect(json_response['name']).to eq(feature_name)
           expect(json_response['state']).to eq('conditional')
           expect(json_response['gates']).to contain_exactly(
             { 'key' => 'boolean', 'value' => false },
@@ -141,13 +169,15 @@ RSpec.describe API::Features, stub_feature_flags: false do
             post api("/features/#{feature_name}", admin), params: { value: 'true', project: project.full_path }
 
             expect(response).to have_gitlab_http_status(:created)
-            expect(json_response).to eq(
-              'name' => 'my_feature',
+            expect(json_response).to match(
+              'name' => feature_name,
               'state' => 'conditional',
               'gates' => [
                 { 'key' => 'boolean', 'value' => false },
                 { 'key' => 'actors', 'value' => ["Project:#{project.id}"] }
-              ])
+              ],
+              'definition' => known_feature_flag_definition_hash
+            )
           end
         end
 
@@ -156,12 +186,13 @@ RSpec.describe API::Features, stub_feature_flags: false do
             post api("/features/#{feature_name}", admin), params: { value: 'true', project: 'mep/to/the/mep/mep' }
 
             expect(response).to have_gitlab_http_status(:created)
-            expect(json_response).to eq(
-              "name" => "my_feature",
+            expect(json_response).to match(
+              "name" => feature_name,
               "state" => "off",
               "gates" => [
                 { "key" => "boolean", "value" => false }
-              ]
+              ],
+              'definition' => known_feature_flag_definition_hash
             )
           end
         end
@@ -175,13 +206,15 @@ RSpec.describe API::Features, stub_feature_flags: false do
             post api("/features/#{feature_name}", admin), params: { value: 'true', group: group.full_path }
 
             expect(response).to have_gitlab_http_status(:created)
-            expect(json_response).to eq(
-              'name' => 'my_feature',
+            expect(json_response).to match(
+              'name' => feature_name,
               'state' => 'conditional',
               'gates' => [
                 { 'key' => 'boolean', 'value' => false },
                 { 'key' => 'actors', 'value' => ["Group:#{group.id}"] }
-              ])
+              ],
+              'definition' => known_feature_flag_definition_hash
+            )
           end
         end
 
@@ -190,12 +223,13 @@ RSpec.describe API::Features, stub_feature_flags: false do
             post api("/features/#{feature_name}", admin), params: { value: 'true', group: 'not/a/group' }
 
             expect(response).to have_gitlab_http_status(:created)
-            expect(json_response).to eq(
-              "name" => "my_feature",
+            expect(json_response).to match(
+              "name" => feature_name,
               "state" => "off",
               "gates" => [
                 { "key" => "boolean", "value" => false }
-              ]
+              ],
+              'definition' => known_feature_flag_definition_hash
             )
           end
         end
@@ -205,26 +239,30 @@ RSpec.describe API::Features, stub_feature_flags: false do
         post api("/features/#{feature_name}", admin), params: { value: '50' }
 
         expect(response).to have_gitlab_http_status(:created)
-        expect(json_response).to eq(
-          'name' => 'my_feature',
+        expect(json_response).to match(
+          'name' => feature_name,
           'state' => 'conditional',
           'gates' => [
             { 'key' => 'boolean', 'value' => false },
             { 'key' => 'percentage_of_time', 'value' => 50 }
-          ])
+          ],
+          'definition' => known_feature_flag_definition_hash
+        )
       end
 
       it 'creates a feature with the given percentage of actors if passed an integer' do
         post api("/features/#{feature_name}", admin), params: { value: '50', key: 'percentage_of_actors' }
 
         expect(response).to have_gitlab_http_status(:created)
-        expect(json_response).to eq(
-          'name' => 'my_feature',
+        expect(json_response).to match(
+          'name' => feature_name,
           'state' => 'conditional',
           'gates' => [
             { 'key' => 'boolean', 'value' => false },
             { 'key' => 'percentage_of_actors', 'value' => 50 }
-          ])
+          ],
+          'definition' => known_feature_flag_definition_hash
+        )
       end
     end
 
@@ -238,36 +276,42 @@ RSpec.describe API::Features, stub_feature_flags: false do
           post api("/features/#{feature_name}", admin), params: { value: 'true' }
 
           expect(response).to have_gitlab_http_status(:created)
-          expect(json_response).to eq(
-            'name' => 'my_feature',
+          expect(json_response).to match(
+            'name' => feature_name,
             'state' => 'on',
-            'gates' => [{ 'key' => 'boolean', 'value' => true }])
+            'gates' => [{ 'key' => 'boolean', 'value' => true }],
+            'definition' => known_feature_flag_definition_hash
+          )
         end
 
         it 'enables the feature for the given Flipper group when passed feature_group=perf_team' do
           post api("/features/#{feature_name}", admin), params: { value: 'true', feature_group: 'perf_team' }
 
           expect(response).to have_gitlab_http_status(:created)
-          expect(json_response).to eq(
-            'name' => 'my_feature',
+          expect(json_response).to match(
+            'name' => feature_name,
             'state' => 'conditional',
             'gates' => [
               { 'key' => 'boolean', 'value' => false },
               { 'key' => 'groups', 'value' => ['perf_team'] }
-            ])
+            ],
+            'definition' => known_feature_flag_definition_hash
+          )
         end
 
         it 'enables the feature for the given user when passed user=username' do
           post api("/features/#{feature_name}", admin), params: { value: 'true', user: user.username }
 
           expect(response).to have_gitlab_http_status(:created)
-          expect(json_response).to eq(
-            'name' => 'my_feature',
+          expect(json_response).to match(
+            'name' => feature_name,
             'state' => 'conditional',
             'gates' => [
               { 'key' => 'boolean', 'value' => false },
               { 'key' => 'actors', 'value' => ["User:#{user.id}"] }
-            ])
+            ],
+            'definition' => known_feature_flag_definition_hash
+          )
         end
       end
 
@@ -279,10 +323,12 @@ RSpec.describe API::Features, stub_feature_flags: false do
           post api("/features/#{feature_name}", admin), params: { value: 'false' }
 
           expect(response).to have_gitlab_http_status(:created)
-          expect(json_response).to eq(
-            'name' => 'my_feature',
+          expect(json_response).to match(
+            'name' => feature_name,
             'state' => 'off',
-            'gates' => [{ 'key' => 'boolean', 'value' => false }])
+            'gates' => [{ 'key' => 'boolean', 'value' => false }],
+            'definition' => known_feature_flag_definition_hash
+          )
         end
 
         it 'disables the feature for the given Flipper group when passed feature_group=perf_team' do
@@ -292,10 +338,12 @@ RSpec.describe API::Features, stub_feature_flags: false do
           post api("/features/#{feature_name}", admin), params: { value: 'false', feature_group: 'perf_team' }
 
           expect(response).to have_gitlab_http_status(:created)
-          expect(json_response).to eq(
-            'name' => 'my_feature',
+          expect(json_response).to match(
+            'name' => feature_name,
             'state' => 'off',
-            'gates' => [{ 'key' => 'boolean', 'value' => false }])
+            'gates' => [{ 'key' => 'boolean', 'value' => false }],
+            'definition' => known_feature_flag_definition_hash
+          )
         end
 
         it 'disables the feature for the given user when passed user=username' do
@@ -305,10 +353,12 @@ RSpec.describe API::Features, stub_feature_flags: false do
           post api("/features/#{feature_name}", admin), params: { value: 'false', user: user.username }
 
           expect(response).to have_gitlab_http_status(:created)
-          expect(json_response).to eq(
-            'name' => 'my_feature',
+          expect(json_response).to match(
+            'name' => feature_name,
             'state' => 'off',
-            'gates' => [{ 'key' => 'boolean', 'value' => false }])
+            'gates' => [{ 'key' => 'boolean', 'value' => false }],
+            'definition' => known_feature_flag_definition_hash
+          )
         end
       end
 
@@ -321,13 +371,15 @@ RSpec.describe API::Features, stub_feature_flags: false do
           post api("/features/#{feature_name}", admin), params: { value: '30' }
 
           expect(response).to have_gitlab_http_status(:created)
-          expect(json_response).to eq(
-            'name' => 'my_feature',
+          expect(json_response).to match(
+            'name' => feature_name,
             'state' => 'conditional',
             'gates' => [
               { 'key' => 'boolean', 'value' => false },
               { 'key' => 'percentage_of_time', 'value' => 30 }
-            ])
+            ],
+            'definition' => known_feature_flag_definition_hash
+          )
         end
       end
 
@@ -340,13 +392,15 @@ RSpec.describe API::Features, stub_feature_flags: false do
           post api("/features/#{feature_name}", admin), params: { value: '74', key: 'percentage_of_actors' }
 
           expect(response).to have_gitlab_http_status(:created)
-          expect(json_response).to eq(
-            'name' => 'my_feature',
+          expect(json_response).to match(
+            'name' => feature_name,
             'state' => 'conditional',
             'gates' => [
               { 'key' => 'boolean', 'value' => false },
               { 'key' => 'percentage_of_actors', 'value' => 74 }
-            ])
+            ],
+            'definition' => known_feature_flag_definition_hash
+          )
         end
       end
     end
diff --git a/spec/support/services/issuable_import_csv_service_shared_examples.rb b/spec/support/services/issuable_import_csv_service_shared_examples.rb
index 20ac2ff5c7c..f68750bec32 100644
--- a/spec/support/services/issuable_import_csv_service_shared_examples.rb
+++ b/spec/support/services/issuable_import_csv_service_shared_examples.rb
@@ -26,29 +26,33 @@ RSpec.shared_examples 'issuable import csv service' do |issuable_type|
     end
   end
 
+  shared_examples_for 'invalid file' do
+    it 'returns invalid file error' do
+      expect(subject[:success]).to eq(0)
+      expect(subject[:parse_error]).to eq(true)
+    end
+
+    it_behaves_like 'importer with email notification'
+    it_behaves_like 'an issuable importer'
+  end
+
   describe '#execute' do
-    context 'invalid file' do
+    context 'invalid file extension' do
       let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
 
-      it 'returns invalid file error' do
-        expect(subject[:success]).to eq(0)
-        expect(subject[:parse_error]).to eq(true)
-      end
+      it_behaves_like 'invalid file'
+    end
 
-      it_behaves_like 'importer with email notification'
-      it_behaves_like 'an issuable importer'
+    context 'empty file' do
+      let(:file) { fixture_file_upload('spec/fixtures/csv_empty.csv') }
+
+      it_behaves_like 'invalid file'
     end
 
     context 'file without headers' do
       let(:file) { fixture_file_upload('spec/fixtures/csv_no_headers.csv') }
 
-      it 'returns invalid file error' do
-        expect(subject[:success]).to eq(0)
-        expect(subject[:parse_error]).to eq(true)
-      end
-
-      it_behaves_like 'importer with email notification'
-      it_behaves_like 'an issuable importer'
+      it_behaves_like 'invalid file'
     end
 
     context 'with a file generated by Gitlab CSV export' do