From 78c8851d4403f878076c698a6b6cc528db019ff3 Mon Sep 17 00:00:00 2001
From: Robert Hunt <rhunt@gitlab.com>
Date: Mon, 26 Jul 2021 14:42:27 +0100
Subject: [PATCH] Add the compliance framework label to group projects listing

- Add compliance framework to entity
- Add compliance framework label component to group item
- Add compliance framework to store
- Create tests

Changelog: added
MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66819
EE: true
---
 .../groups/components/group_item.vue          |  17 +++
 .../javascripts/groups/store/groups_store.js  |  15 +-
 ee/app/serializers/ee/group_child_entity.rb   |  10 ++
 .../groups/components/group_item_spec.js      |  48 ++++++
 ee/spec/frontend/groups/mock_data.js          | 140 ++++++++++++++++++
 .../groups/store/groups_store_spec.js         |  24 +++
 .../serializers/ee/group_child_entity_spec.rb |  75 ++++++++++
 .../groups/components/group_item_spec.js      |   5 +-
 8 files changed, 329 insertions(+), 5 deletions(-)
 create mode 100644 ee/spec/frontend/groups/components/group_item_spec.js
 create mode 100644 ee/spec/frontend/groups/mock_data.js
 create mode 100644 ee/spec/frontend/groups/store/groups_store_spec.js
 create mode 100644 ee/spec/serializers/ee/group_child_entity_spec.rb

diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index ad0b27c969379..10c45abbfa2c3 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -28,6 +28,10 @@ export default {
     GlLoadingIcon,
     GlIcon,
     UserAccessRoleBadge,
+    ComplianceFrameworkLabel: () =>
+      import(
+        'ee_component/vue_shared/components/compliance_framework_label/compliance_framework_label.vue'
+      ),
     itemCaret,
     itemTypeIcon,
     itemStats,
@@ -67,6 +71,9 @@ export default {
     hasAvatar() {
       return this.group.avatarUrl !== null;
     },
+    hasComplianceFramework() {
+      return Boolean(this.group.complianceFramework?.name);
+    },
     isGroup() {
       return this.group.type === 'group';
     },
@@ -82,6 +89,9 @@ export default {
     microdata() {
       return this.group.microdata || {};
     },
+    complianceFramework() {
+      return this.group.complianceFramework;
+    },
   },
   methods: {
     onClickRowGroup(e) {
@@ -167,6 +177,13 @@ export default {
             <user-access-role-badge v-if="group.permission" class="gl-mt-3">
               {{ group.permission }}
             </user-access-role-badge>
+            <compliance-framework-label
+              v-if="hasComplianceFramework"
+              class="gl-mt-3"
+              :name="complianceFramework.name"
+              :color="complianceFramework.color"
+              :description="complianceFramework.description"
+            />
           </div>
           <div v-if="group.description" class="description">
             <span
diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js
index 6cf70f4052ef8..93fbd8be47d5d 100644
--- a/app/assets/javascripts/groups/store/groups_store.js
+++ b/app/assets/javascripts/groups/store/groups_store.js
@@ -1,4 +1,5 @@
-import { normalizeHeaders, parseIntPagination } from '../../lib/utils/common_utils';
+import { isEmpty } from 'lodash';
+import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
 import { getGroupItemMicrodata } from './utils';
 
 export default class GroupsStore {
@@ -70,7 +71,7 @@ export default class GroupsStore {
       ? rawGroupItem.subgroup_count
       : rawGroupItem.children_count;
 
-    return {
+    const groupItem = {
       id: rawGroupItem.id,
       name: rawGroupItem.name,
       fullName: rawGroupItem.full_name,
@@ -98,6 +99,16 @@ export default class GroupsStore {
       pendingRemoval: rawGroupItem.marked_for_deletion,
       microdata: this.showSchemaMarkup ? getGroupItemMicrodata(rawGroupItem) : {},
     };
+
+    if (!isEmpty(rawGroupItem.compliance_management_framework)) {
+      groupItem.complianceFramework = {
+        name: rawGroupItem.compliance_management_framework.name,
+        color: rawGroupItem.compliance_management_framework.color,
+        description: rawGroupItem.compliance_management_framework.description,
+      };
+    }
+
+    return groupItem;
   }
 
   removeGroup(group, parentGroup) {
diff --git a/ee/app/serializers/ee/group_child_entity.rb b/ee/app/serializers/ee/group_child_entity.rb
index 4a53f0ff62014..4c414a51c4415 100644
--- a/ee/app/serializers/ee/group_child_entity.rb
+++ b/ee/app/serializers/ee/group_child_entity.rb
@@ -9,6 +9,16 @@ module GroupChildEntity
       expose :marked_for_deletion do |instance|
         instance.marked_for_deletion?
       end
+
+      expose :compliance_management_framework, if: lambda { |_instance, _options| compliance_framework_available? }
+    end
+
+    private
+
+    def compliance_framework_available?
+      return unless project?
+
+      object.licensed_feature_available?(:compliance_framework)
     end
   end
 end
diff --git a/ee/spec/frontend/groups/components/group_item_spec.js b/ee/spec/frontend/groups/components/group_item_spec.js
new file mode 100644
index 0000000000000..7d59805a3fd8f
--- /dev/null
+++ b/ee/spec/frontend/groups/components/group_item_spec.js
@@ -0,0 +1,48 @@
+import { shallowMount } from '@vue/test-utils';
+import ComplianceFrameworkLabel from 'ee_component/vue_shared/components/compliance_framework_label/compliance_framework_label.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import GroupFolder from '~/groups/components/group_folder.vue';
+import GroupItem from '~/groups/components/group_item.vue';
+import { mockParentGroupItem, mockChildren } from '../mock_data';
+
+const createComponent = (props = {}) => {
+  return shallowMount(GroupItem, {
+    propsData: {
+      parentGroup: mockParentGroupItem,
+      ...props,
+    },
+    components: { GroupFolder },
+  });
+};
+
+describe('GroupItemComponent', () => {
+  let wrapper;
+
+  const findComplianceFrameworkLabel = () => wrapper.findComponent(ComplianceFrameworkLabel);
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  describe('Compliance framework label', () => {
+    it('does not render if the item does not have a compliance framework', async () => {
+      wrapper = createComponent({ group: mockChildren[0] });
+      await waitForPromises();
+
+      expect(findComplianceFrameworkLabel().exists()).toBe(false);
+    });
+
+    it('renders if the item has a compliance framework', async () => {
+      const { color, description, name } = mockChildren[1].complianceFramework;
+
+      wrapper = createComponent({ group: mockChildren[1] });
+      await waitForPromises();
+
+      expect(findComplianceFrameworkLabel().props()).toStrictEqual({
+        color,
+        description,
+        name,
+      });
+    });
+  });
+});
diff --git a/ee/spec/frontend/groups/mock_data.js b/ee/spec/frontend/groups/mock_data.js
new file mode 100644
index 0000000000000..49dda29825f3c
--- /dev/null
+++ b/ee/spec/frontend/groups/mock_data.js
@@ -0,0 +1,140 @@
+export const mockParentGroupItem = {
+  id: 55,
+  name: 'hardware',
+  description: '',
+  visibility: 'public',
+  fullName: 'platform / hardware',
+  relativePath: '/platform/hardware',
+  canEdit: true,
+  type: 'group',
+  avatarUrl: null,
+  permission: 'Owner',
+  editPath: '/groups/platform/hardware/edit',
+  childrenCount: 3,
+  leavePath: '/groups/platform/hardware/group_members/leave',
+  parentId: 54,
+  memberCount: '1',
+  projectCount: 1,
+  subgroupCount: 2,
+  canLeave: false,
+  children: [],
+  isOpen: true,
+  isChildrenLoading: false,
+  isBeingRemoved: false,
+  updatedAt: '2017-04-09T18:40:39.101Z',
+};
+
+export const mockChildren = [
+  {
+    id: 57,
+    name: 'bsp',
+    description: '',
+    visibility: 'public',
+    fullName: 'platform / hardware / bsp',
+    relativePath: '/platform/hardware/bsp',
+    canEdit: true,
+    type: 'group',
+    avatarUrl: null,
+    permission: 'Owner',
+    editPath: '/groups/platform/hardware/bsp/edit',
+    childrenCount: 6,
+    leavePath: '/groups/platform/hardware/bsp/group_members/leave',
+    parentId: 55,
+    memberCount: '1',
+    projectCount: 4,
+    subgroupCount: 2,
+    canLeave: false,
+    children: [],
+    isOpen: true,
+    isChildrenLoading: false,
+    isBeingRemoved: false,
+    updatedAt: '2017-04-09T18:40:39.101Z',
+    complianceFramework: {},
+  },
+  {
+    id: 57,
+    name: 'bsp',
+    description: '',
+    visibility: 'public',
+    fullName: 'platform / hardware / bsp',
+    relativePath: '/platform/hardware/bsp',
+    canEdit: true,
+    type: 'group',
+    avatarUrl: null,
+    permission: 'Owner',
+    editPath: '/groups/platform/hardware/bsp/edit',
+    childrenCount: 6,
+    leavePath: '/groups/platform/hardware/bsp/group_members/leave',
+    parentId: 55,
+    memberCount: '1',
+    projectCount: 4,
+    subgroupCount: 2,
+    canLeave: false,
+    children: [],
+    isOpen: true,
+    isChildrenLoading: false,
+    isBeingRemoved: false,
+    updatedAt: '2017-04-09T18:40:39.101Z',
+    complianceFramework: {
+      name: 'GDPR',
+      description: 'General Data Protection Regulation',
+      color: '#009966',
+    },
+  },
+];
+
+export const mockRawChildren = [
+  {
+    id: 57,
+    name: 'bsp',
+    description: '',
+    visibility: 'public',
+    full_name: 'platform / hardware / bsp',
+    relative_path: '/platform/hardware/bsp',
+    can_edit: true,
+    type: 'group',
+    avatar_url: null,
+    permission: 'Owner',
+    edit_path: '/groups/platform/hardware/bsp/edit',
+    children_count: 6,
+    leave_path: '/groups/platform/hardware/bsp/group_members/leave',
+    parent_id: 55,
+    number_users_with_delimiter: '1',
+    project_count: 4,
+    subgroup_count: 2,
+    can_leave: false,
+    children: [],
+    updated_at: '2017-04-09T18:40:39.101Z',
+  },
+  {
+    id: 57,
+    name: 'bsp',
+    description: '',
+    visibility: 'public',
+    full_name: 'platform / hardware / bsp',
+    relative_path: '/platform/hardware/bsp',
+    can_edit: true,
+    type: 'group',
+    avatar_url: null,
+    permission: 'Owner',
+    edit_path: '/groups/platform/hardware/bsp/edit',
+    children_count: 6,
+    leave_path: '/groups/platform/hardware/bsp/group_members/leave',
+    parent_id: 55,
+    number_users_with_delimiter: '1',
+    project_count: 4,
+    subgroup_count: 2,
+    can_leave: false,
+    children: [],
+    updated_at: '2017-04-09T18:40:39.101Z',
+    compliance_management_framework: {
+      id: 1,
+      namespace_id: 1,
+      name: 'GDPR',
+      description: 'General Data Protection Regulation',
+      color: '#009966',
+      pipeline_configuration_full_path: null,
+      regulated: true,
+    },
+  },
+];
diff --git a/ee/spec/frontend/groups/store/groups_store_spec.js b/ee/spec/frontend/groups/store/groups_store_spec.js
new file mode 100644
index 0000000000000..bcfe11bad5f85
--- /dev/null
+++ b/ee/spec/frontend/groups/store/groups_store_spec.js
@@ -0,0 +1,24 @@
+import GroupsStore from '~/groups/store/groups_store';
+import { mockRawChildren } from '../mock_data';
+
+describe('ee/ProjectsStore', () => {
+  describe('formatGroupItem', () => {
+    it('without a compliance framework', () => {
+      const store = new GroupsStore();
+      const updatedGroupItem = store.formatGroupItem(mockRawChildren[0]);
+
+      expect(updatedGroupItem.complianceFramework).toBeUndefined();
+    });
+
+    it('with a compliance framework', () => {
+      const store = new GroupsStore();
+      const updatedGroupItem = store.formatGroupItem(mockRawChildren[1]);
+
+      expect(updatedGroupItem.complianceFramework).toStrictEqual({
+        name: mockRawChildren[1].compliance_management_framework.name,
+        color: mockRawChildren[1].compliance_management_framework.color,
+        description: mockRawChildren[1].compliance_management_framework.description,
+      });
+    });
+  });
+});
diff --git a/ee/spec/serializers/ee/group_child_entity_spec.rb b/ee/spec/serializers/ee/group_child_entity_spec.rb
new file mode 100644
index 0000000000000..c593b69406abc
--- /dev/null
+++ b/ee/spec/serializers/ee/group_child_entity_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GroupChildEntity do
+  include ExternalAuthorizationServiceHelpers
+  include Gitlab::Routing.url_helpers
+
+  let_it_be(:user) { create(:user) }
+  let_it_be(:project) { create(:project, :with_sox_compliance_framework) }
+  let_it_be(:project_without_compliance_framework) { create(:project) }
+  let_it_be(:group) { create(:group) }
+
+  let(:request) { double('request') }
+  let(:entity) { described_class.new(object, request: request) }
+
+  subject(:json) { entity.as_json }
+
+  before do
+    allow(request).to receive(:current_user).and_return(user)
+    stub_commonmark_sourcepos_disabled
+  end
+
+  describe 'with compliance framework' do
+    shared_examples 'does not have the compliance framework' do
+      it do
+        expect(json[:compliance_management_framework]).to be_nil
+      end
+    end
+
+    context 'disabled' do
+      before do
+        stub_licensed_features(compliance_framework: false)
+      end
+
+      context 'for a project' do
+        let(:object) { project }
+
+        it_behaves_like 'does not have the compliance framework'
+      end
+
+      context 'for a group' do
+        let(:object) { group }
+
+        it_behaves_like 'does not have the compliance framework'
+      end
+    end
+
+    describe 'enabled' do
+      before do
+        stub_licensed_features(compliance_framework: true)
+      end
+
+      context 'for a project' do
+        let(:object) { project }
+
+        it 'has the compliance framework' do
+          expect(json[:compliance_management_framework]['name']).to eq('SOX')
+        end
+      end
+
+      context 'for a project without a compliance framework' do
+        let(:object) { project_without_compliance_framework }
+
+        it_behaves_like 'does not have the compliance framework'
+      end
+
+      context 'for a group' do
+        let(:object) { group }
+
+        it_behaves_like 'does not have the compliance framework'
+      end
+    end
+  end
+end
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index 2369685f50626..60d47895a959d 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -1,5 +1,5 @@
 import { mount } from '@vue/test-utils';
-import Vue from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
 import GroupFolder from '~/groups/components/group_folder.vue';
 import GroupItem from '~/groups/components/group_item.vue';
 import ItemActions from '~/groups/components/item_actions.vue';
@@ -22,8 +22,7 @@ describe('GroupItemComponent', () => {
 
   beforeEach(() => {
     wrapper = createComponent();
-
-    return Vue.nextTick();
+    return waitForPromises();
   });
 
   afterEach(() => {
-- 
GitLab