From 00d201e4c8df24b4a3a1da982d85fa641f0e28b1 Mon Sep 17 00:00:00 2001
From: Mark Lapierre <mlapierre@gitlab.com>
Date: Thu, 15 Dec 2022 05:32:05 +0000
Subject: [PATCH] Add contract test for suggested reviewers dropdown

- Adds new packages to packages.json to give contract tests equal
  value to unit/feature tests.
- Adds the contract test to ee/ and modifies spec_helper in
  spec/contracts/provider to allow EE tests to run.
- Adds jest script to run contract tests.
- Adds rake task to run provider tests.
---
 .eslintrc.yml                                 |  2 +-
 .../testing_guide/contract/index.md           |  2 +
 ee/lib/tasks/contracts/merge_requests.rake    | 30 ++++++++
 ee/spec/contracts/.gitignore                  |  2 +
 ee/spec/contracts/consumer/.eslintrc.yml      |  7 ++
 .../suggested_reviewers.fixture.js            | 56 ++++++++++++++
 .../api/project/autocomplete_users.js         | 24 ++++++
 .../specs/project/merge_request/show.spec.js  | 41 ++++++++++
 ee/spec/contracts/consumer/test_constants.js  |  3 +
 ..._request_suggested_reviewers_endpoint.json | 77 +++++++++++++++++++
 .../show/suggested_reviewers_helper.rb        | 18 +++++
 ee/spec/contracts/provider/spec_helper.rb     |  9 +++
 .../project/merge_request/show_state.rb       | 23 ++++++
 jest.config.base.js                           |  2 +-
 jest.config.contract.js                       |  6 ++
 package.json                                  |  1 +
 spec/contracts/consumer/.node-version         |  1 -
 spec/contracts/consumer/package.json          |  3 +
 spec/contracts/provider/spec_helper.rb        |  9 +++
 19 files changed, 313 insertions(+), 3 deletions(-)
 create mode 100644 ee/lib/tasks/contracts/merge_requests.rake
 create mode 100644 ee/spec/contracts/.gitignore
 create mode 100644 ee/spec/contracts/consumer/.eslintrc.yml
 create mode 100644 ee/spec/contracts/consumer/fixtures/project/merge_request/suggested_reviewers.fixture.js
 create mode 100644 ee/spec/contracts/consumer/resources/api/project/autocomplete_users.js
 create mode 100644 ee/spec/contracts/consumer/specs/project/merge_request/show.spec.js
 create mode 100644 ee/spec/contracts/consumer/test_constants.js
 create mode 100644 ee/spec/contracts/contracts/project/merge_request/show/mergerequest#show-merge_request_suggested_reviewers_endpoint.json
 create mode 100644 ee/spec/contracts/provider/pact_helpers/project/merge_request/show/suggested_reviewers_helper.rb
 create mode 100644 ee/spec/contracts/provider/spec_helper.rb
 create mode 100644 ee/spec/contracts/provider/states/project/merge_request/show_state.rb
 create mode 100644 jest.config.contract.js
 delete mode 100644 spec/contracts/consumer/.node-version

diff --git a/.eslintrc.yml b/.eslintrc.yml
index d2bae1b21b34..4a7197e3bd5c 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -193,6 +193,6 @@ overrides:
       '@graphql-eslint/no-unused-fragments': error
       '@graphql-eslint/no-duplicate-fields': error
   - files:
-    - 'spec/contracts/consumer/**/*'
+    - '{,ee/}spec/contracts/consumer/**/*'
     rules:
       '@gitlab/require-i18n-strings': off
diff --git a/doc/development/testing_guide/contract/index.md b/doc/development/testing_guide/contract/index.md
index 8412a260c7d4..08a21e58a528 100644
--- a/doc/development/testing_guide/contract/index.md
+++ b/doc/development/testing_guide/contract/index.md
@@ -26,6 +26,8 @@ The contracts themselves are stored in [`/spec/contracts/contracts`](https://git
 
 Before running the consumer tests, go to `spec/contracts/consumer` and run `npm install`. To run all the consumer tests, you just need to run `npm test -- /specs`. Otherwise, to run a specific spec file, replace `/specs` with the specific spec filename.
 
+You can also run tests from the root directory of the project, using the command `yarn jest:contract`.
+
 ### Run the provider tests
 
 Before running the provider tests, make sure your GDK (GitLab Development Kit) is fully set up and running. You can follow the setup instructions detailed in the [GDK repository](https://gitlab.com/gitlab-org/gitlab-development-kit/-/tree/main). To run the provider tests, you use Rake tasks that can be found in [`./lib/tasks/contracts`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/tasks/contracts). To get a list of all the Rake tasks related to the provider tests, run `bundle exec rake -T contracts`. For example:
diff --git a/ee/lib/tasks/contracts/merge_requests.rake b/ee/lib/tasks/contracts/merge_requests.rake
new file mode 100644
index 000000000000..6017be8d7b85
--- /dev/null
+++ b/ee/lib/tasks/contracts/merge_requests.rake
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+return if Rails.env.production?
+
+require 'pact/tasks/verification_task'
+
+contracts = File.expand_path('../../../spec/contracts/contracts/project/merge_request', __dir__)
+provider = File.expand_path('../../../spec/contracts/provider', __dir__)
+
+namespace :contracts do
+  namespace :merge_requests do
+    Pact::VerificationTask.new(:suggested_reviewers) do |pact|
+      pact.uri(
+        "#{contracts}/show/mergerequest#show-merge_request_suggested_reviewers_endpoint.json",
+        pact_helper: "#{provider}/pact_helpers/project/merge_request/show/suggested_reviewers_helper.rb"
+      )
+    end
+
+    desc 'Run all merge request contract tests'
+    task 'test:merge_requests', :contract_merge_requests do |_t, arg|
+      errors = %w[suggested_reviewers].each_with_object([]) do |task, err|
+        Rake::Task["contracts:merge_requests:pact:verify:#{task}"].execute
+      rescue StandardError, SystemExit
+        err << "contracts:merge_requests:pact:verify:#{task}"
+      end
+
+      raise StandardError, "Errors in tasks #{errors.join(', ')}" unless errors.empty?
+    end
+  end
+end
diff --git a/ee/spec/contracts/.gitignore b/ee/spec/contracts/.gitignore
new file mode 100644
index 000000000000..cb89d4102d38
--- /dev/null
+++ b/ee/spec/contracts/.gitignore
@@ -0,0 +1,2 @@
+logs/
+consumer/node_modules
diff --git a/ee/spec/contracts/consumer/.eslintrc.yml b/ee/spec/contracts/consumer/.eslintrc.yml
new file mode 100644
index 000000000000..e4b380714d35
--- /dev/null
+++ b/ee/spec/contracts/consumer/.eslintrc.yml
@@ -0,0 +1,7 @@
+---
+extends:
+  - 'plugin:@gitlab/jest'
+settings:
+  import/core-modules:
+    - '@pact-foundation/pact'
+    - jest-pact
diff --git a/ee/spec/contracts/consumer/fixtures/project/merge_request/suggested_reviewers.fixture.js b/ee/spec/contracts/consumer/fixtures/project/merge_request/suggested_reviewers.fixture.js
new file mode 100644
index 000000000000..06841998c974
--- /dev/null
+++ b/ee/spec/contracts/consumer/fixtures/project/merge_request/suggested_reviewers.fixture.js
@@ -0,0 +1,56 @@
+import { Matchers } from '@pact-foundation/pact';
+import {
+  AUTOCOMPLETE_USERS_URL,
+  TEST_PROJECT_ID,
+  TEST_MERGE_REQUEST_IID,
+} from '../../../test_constants';
+
+const userIdMatchExample1 = 6954442;
+const userIdMatchExample2 = 6954441;
+
+const body = [
+  {
+    id: Matchers.integer(userIdMatchExample1),
+    username: Matchers.string('user1'),
+    name: Matchers.string('A User'),
+  },
+  {
+    id: Matchers.integer(userIdMatchExample2),
+    username: Matchers.string('gitlab-qa'),
+    name: Matchers.string('Contract Test User'),
+    suggested: Matchers.boolean(true),
+  },
+];
+
+export const suggestedReviewersFixture = {
+  body: Matchers.extractPayload(body),
+
+  success: {
+    status: 200,
+    headers: {
+      'Content-Type': 'application/json; charset=utf-8',
+    },
+    body,
+  },
+
+  scenario: {
+    state: 'a merge request exists with suggested reviewers available for selection',
+    uponReceiving: 'a request for suggested reviewers',
+  },
+
+  request: {
+    withRequest: {
+      method: 'GET',
+      path: AUTOCOMPLETE_USERS_URL,
+      query: {
+        active: 'true',
+        project_id: Matchers.string(TEST_PROJECT_ID),
+        merge_request_iid: Matchers.string(TEST_MERGE_REQUEST_IID),
+        current_user: 'true',
+      },
+      headers: {
+        Accept: '*/*',
+      },
+    },
+  },
+};
diff --git a/ee/spec/contracts/consumer/resources/api/project/autocomplete_users.js b/ee/spec/contracts/consumer/resources/api/project/autocomplete_users.js
new file mode 100644
index 000000000000..875e39bba5de
--- /dev/null
+++ b/ee/spec/contracts/consumer/resources/api/project/autocomplete_users.js
@@ -0,0 +1,24 @@
+import axios from 'axios';
+
+import {
+  AUTOCOMPLETE_USERS_URL,
+  TEST_PROJECT_ID,
+  TEST_MERGE_REQUEST_IID,
+} from '../../../test_constants';
+
+export async function getSuggestedReviewers(endpoint) {
+  const { url } = endpoint;
+
+  return axios({
+    method: 'GET',
+    baseURL: url,
+    url: AUTOCOMPLETE_USERS_URL,
+    headers: { Accept: '*/*' },
+    params: {
+      active: 'true',
+      project_id: TEST_PROJECT_ID,
+      merge_request_iid: TEST_MERGE_REQUEST_IID,
+      current_user: 'true',
+    },
+  }).then((response) => response.data);
+}
diff --git a/ee/spec/contracts/consumer/specs/project/merge_request/show.spec.js b/ee/spec/contracts/consumer/specs/project/merge_request/show.spec.js
new file mode 100644
index 000000000000..a0ef108ed5a9
--- /dev/null
+++ b/ee/spec/contracts/consumer/specs/project/merge_request/show.spec.js
@@ -0,0 +1,41 @@
+import path from 'path';
+import { pactWith } from 'jest-pact';
+import { suggestedReviewersFixture } from '../../../fixtures/project/merge_request/suggested_reviewers.fixture';
+import { getSuggestedReviewers } from '../../../resources/api/project/autocomplete_users';
+
+const ROOT_PATH = path.resolve(__dirname, '../../..');
+const CONSUMER_NAME = 'MergeRequest#show';
+const CONSUMER_LOG = path.join(ROOT_PATH, '../logs/consumer.log');
+const CONTRACT_DIR = path.join(ROOT_PATH, '../contracts/project/merge_request/show');
+const SUGGESTED_REVIEWERS_PROVIDER_NAME = 'Merge Request Suggested Reviewers Endpoint';
+
+// API endpoint: /autocomplete/users.json
+pactWith(
+  {
+    consumer: CONSUMER_NAME,
+    provider: SUGGESTED_REVIEWERS_PROVIDER_NAME,
+    log: CONSUMER_LOG,
+    dir: CONTRACT_DIR,
+  },
+
+  (provider) => {
+    describe(SUGGESTED_REVIEWERS_PROVIDER_NAME, () => {
+      beforeEach(() => {
+        const interaction = {
+          ...suggestedReviewersFixture.scenario,
+          ...suggestedReviewersFixture.request,
+          willRespondWith: suggestedReviewersFixture.success,
+        };
+        provider.addInteraction(interaction);
+      });
+
+      it('return a successful body', async () => {
+        const suggestedReviewers = await getSuggestedReviewers({
+          url: provider.mockService.baseUrl,
+        });
+
+        expect(suggestedReviewers).toEqual(suggestedReviewersFixture.body);
+      });
+    });
+  },
+);
diff --git a/ee/spec/contracts/consumer/test_constants.js b/ee/spec/contracts/consumer/test_constants.js
new file mode 100644
index 000000000000..c1db52945baf
--- /dev/null
+++ b/ee/spec/contracts/consumer/test_constants.js
@@ -0,0 +1,3 @@
+export const AUTOCOMPLETE_USERS_URL = '/-/autocomplete/users.json';
+export const TEST_PROJECT_ID = '12345';
+export const TEST_MERGE_REQUEST_IID = '54321';
diff --git a/ee/spec/contracts/contracts/project/merge_request/show/mergerequest#show-merge_request_suggested_reviewers_endpoint.json b/ee/spec/contracts/contracts/project/merge_request/show/mergerequest#show-merge_request_suggested_reviewers_endpoint.json
new file mode 100644
index 000000000000..e52945acb6bb
--- /dev/null
+++ b/ee/spec/contracts/contracts/project/merge_request/show/mergerequest#show-merge_request_suggested_reviewers_endpoint.json
@@ -0,0 +1,77 @@
+{
+  "consumer": {
+    "name": "MergeRequest#show"
+  },
+  "provider": {
+    "name": "Merge Request Suggested Reviewers Endpoint"
+  },
+  "interactions": [
+    {
+      "description": "a request for suggested reviewers",
+      "providerState": "a merge request exists with suggested reviewers available for selection",
+      "request": {
+        "method": "GET",
+        "path": "/-/autocomplete/users.json",
+        "query": "active=true&project_id=12345&merge_request_iid=54321&current_user=true",
+        "headers": {
+          "Accept": "*/*"
+        },
+        "matchingRules": {
+          "$.query.project_id[0]": {
+            "match": "type"
+          },
+          "$.query.merge_request_iid[0]": {
+            "match": "type"
+          }
+        }
+      },
+      "response": {
+        "status": 200,
+        "headers": {
+          "Content-Type": "application/json; charset=utf-8"
+        },
+        "body": [
+          {
+            "id": 6954442,
+            "username": "user1",
+            "name": "A User"
+          },
+          {
+            "id": 6954441,
+            "username": "gitlab-qa",
+            "name": "Contract Test User",
+            "suggested": true
+          }
+        ],
+        "matchingRules": {
+          "$.body[0].id": {
+            "match": "type"
+          },
+          "$.body[0].username": {
+            "match": "type"
+          },
+          "$.body[0].name": {
+            "match": "type"
+          },
+          "$.body[1].id": {
+            "match": "type"
+          },
+          "$.body[1].username": {
+            "match": "type"
+          },
+          "$.body[1].name": {
+            "match": "type"
+          },
+          "$.body[1].suggested": {
+            "match": "type"
+          }
+        }
+      }
+    }
+  ],
+  "metadata": {
+    "pactSpecification": {
+      "version": "2.0.0"
+    }
+  }
+}
\ No newline at end of file
diff --git a/ee/spec/contracts/provider/pact_helpers/project/merge_request/show/suggested_reviewers_helper.rb b/ee/spec/contracts/provider/pact_helpers/project/merge_request/show/suggested_reviewers_helper.rb
new file mode 100644
index 000000000000..b0ad789aa862
--- /dev/null
+++ b/ee/spec/contracts/provider/pact_helpers/project/merge_request/show/suggested_reviewers_helper.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../../../states/project/merge_request/show_state'
+require_relative '../../../../../../../../spec/contracts/provider/spec_helper'
+require_relative '../../../../../../../../spec/contracts/provider/environments/test'
+
+module Provider
+  module SuggestedReviewersHelper
+    Pact.service_provider "Merge Request Suggested Reviewers Endpoint" do
+      app { Environments::Test.app }
+
+      honours_pact_with 'MergeRequest#show' do
+        pact_uri '../contracts/project/merge_request/show/mergerequest#show-merge_request_suggested_reviewers_endpoint.json' # rubocop:disable Layout/LineLength
+      end
+    end
+  end
+end
diff --git a/ee/spec/contracts/provider/spec_helper.rb b/ee/spec/contracts/provider/spec_helper.rb
new file mode 100644
index 000000000000..ea703683987c
--- /dev/null
+++ b/ee/spec/contracts/provider/spec_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require Rails.root.join("config/initializers/0_inject_enterprise_edition_module.rb")
+require Rails.root.join("ee/spec/support/helpers/ee/license_helpers.rb")
+require Rails.root.join("spec/support/helpers/license_helper.rb")
+
+Pact.configure do |config|
+  config.include LicenseHelpers
+end
diff --git a/ee/spec/contracts/provider/states/project/merge_request/show_state.rb b/ee/spec/contracts/provider/states/project/merge_request/show_state.rb
new file mode 100644
index 000000000000..7c67af8fce06
--- /dev/null
+++ b/ee/spec/contracts/provider/states/project/merge_request/show_state.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+Pact.provider_states_for "MergeRequest#show" do
+  provider_state "a merge request exists with suggested reviewers available for selection" do
+    set_up do
+      # Suggested Reviewers is a SaaS feature, but we can't use the `:saas` RSpec metadata like we do in other specs
+      allow(::Gitlab).to receive(:com?).and_return(true)
+
+      stub_licensed_features(suggested_reviewers: true)
+      stub_feature_flags(suggested_reviewers_control: true)
+
+      user = User.find_by(name: Provider::UsersHelper::CONTRACT_USER_NAME)
+      namespace = create(:namespace, name: 'gitlab-org')
+      project = create(:project, id: 12345, name: 'gitlab-qa', namespace: namespace)
+      project.add_maintainer(user)
+      project.project_setting.update!(suggested_reviewers_enabled: true)
+      merge_request = create(:merge_request, iid: 54321, source_project: project, author: user)
+
+      merge_request.build_predictions
+      merge_request.predictions.update!(suggested_reviewers: { reviewers: [user.username] })
+    end
+  end
+end
diff --git a/jest.config.base.js b/jest.config.base.js
index de9bff774e18..05967b51b88d 100644
--- a/jest.config.base.js
+++ b/jest.config.base.js
@@ -26,7 +26,7 @@ module.exports = (path, options = {}) => {
     ]);
   }
 
-  const glob = `${path}/**/*_spec.js`;
+  const glob = `${path}/**/*@([._])spec.js`;
   let testMatch = [`<rootDir>/${glob}`];
   if (IS_EE) {
     testMatch.push(`<rootDir>/ee/${glob}`);
diff --git a/jest.config.contract.js b/jest.config.contract.js
new file mode 100644
index 000000000000..224d50f87d65
--- /dev/null
+++ b/jest.config.contract.js
@@ -0,0 +1,6 @@
+module.exports = () => {
+  return {
+    modulePaths: ['<rootDir>/spec/contracts/consumer/node_modules/'],
+    roots: ['spec/contracts/consumer', 'ee/spec/contracts/consumer'],
+  };
+};
diff --git a/package.json b/package.json
index f1fe4b54d933..8b8af78c47af 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
     "jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
     "jest:ci": "jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/parallel_ci_sequencer.js",
     "jest:ci:minimal": "jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/parallel_ci_sequencer.js",
+    "jest:contract": "PACT_DO_NOT_TRACK=true jest --config jest.config.contract.js --runInBand",
     "jest:integration": "jest --config jest.config.integration.js",
     "lint:eslint": "node scripts/frontend/eslint.js",
     "lint:eslint:fix": "node scripts/frontend/eslint.js --fix",
diff --git a/spec/contracts/consumer/.node-version b/spec/contracts/consumer/.node-version
deleted file mode 100644
index 18711d290eac..000000000000
--- a/spec/contracts/consumer/.node-version
+++ /dev/null
@@ -1 +0,0 @@
-14.17.5
diff --git a/spec/contracts/consumer/package.json b/spec/contracts/consumer/package.json
index 6d3feaa6d4c8..60f268806de1 100644
--- a/spec/contracts/consumer/package.json
+++ b/spec/contracts/consumer/package.json
@@ -22,5 +22,8 @@
   "devDependencies": {
     "@babel/preset-env": "^7.18.2",
     "babel-jest": "^28.1.1"
+  },
+  "config": {
+    "pact_do_not_track": true
   }
 }
diff --git a/spec/contracts/provider/spec_helper.rb b/spec/contracts/provider/spec_helper.rb
index 6009d6524e11..44e4d29c18ea 100644
--- a/spec/contracts/provider/spec_helper.rb
+++ b/spec/contracts/provider/spec_helper.rb
@@ -3,6 +3,13 @@
 require 'spec_helper'
 require 'zeitwerk'
 require_relative 'helpers/users_helper'
+require_relative('../../../ee/spec/contracts/provider/spec_helper') if Gitlab.ee?
+require Rails.root.join("spec/support/helpers/rails_helpers.rb")
+require Rails.root.join("spec/support/helpers/stub_env.rb")
+
+# Opt out of telemetry collection. We can't allow all engineers, and users who install GitLab from source, to be
+# automatically enrolled in sending data on their usage without their knowledge.
+ENV['PACT_DO_NOT_TRACK'] = 'true'
 
 RSpec.configure do |config|
   config.include Devise::Test::IntegrationHelpers
@@ -19,6 +26,8 @@
 
 Pact.configure do |config|
   config.include FactoryBot::Syntax::Methods
+  config.include RailsHelpers
+  config.include StubENV
 end
 
 module SpecHelper
-- 
GitLab