Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-21 09:09:48 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-21 09:09:48 +0300
commitf9441cac3defdec8bdc34cefe7b4364fb49c0aff (patch)
tree372bbc9d0f33acd2c16095d713734ed85277bbfd
parent98e4ee99fe0ae9a8563d223c5cb7f0752d4a2604 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue16
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/history_items.vue10
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/reported_content.vue13
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/user_details.vue40
-rw-r--r--app/assets/javascripts/admin/abuse_report/constants.js4
-rw-r--r--app/finders/organizations/groups_finder.rb59
-rw-r--r--app/graphql/resolvers/organizations/groups_resolver.rb37
-rw-r--r--app/graphql/resolvers/organizations/organization_resolver.rb22
-rw-r--r--app/graphql/types/organizations/group_sort_enum.rb24
-rw-r--r--app/graphql/types/organizations/organization_type.rb33
-rw-r--r--app/graphql/types/query_type.rb6
-rw-r--r--app/models/abuse_report.rb14
-rw-r--r--app/models/namespace.rb1
-rw-r--r--app/models/organizations/organization.rb2
-rw-r--r--app/serializers/admin/abuse_report_details_entity.rb50
-rw-r--r--app/serializers/admin/reported_content_entity.rb38
-rw-r--r--config/feature_flags/development/resolve_organization_groups.yml8
-rw-r--r--db/migrate/20230807035953_add_index_to_abuse_reports_on_user_id_status_and_category.rb15
-rw-r--r--db/schema_migrations/202308070359531
-rw-r--r--db/structure.sql2
-rw-r--r--doc/api/graphql/reference/index.md72
-rw-r--r--doc/architecture/blueprints/organization/index.md23
-rw-r--r--doc/architecture/blueprints/work_items/index.md36
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml2
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/finders/organizations/groups_finder_spec.rb86
-rw-r--r--spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js18
-rw-r--r--spec/frontend/admin/abuse_report/components/history_items_spec.js7
-rw-r--r--spec/frontend/admin/abuse_report/components/reported_content_spec.js11
-rw-r--r--spec/frontend/admin/abuse_report/components/user_details_spec.js62
-rw-r--r--spec/frontend/admin/abuse_report/mock_data.js33
-rw-r--r--spec/graphql/types/organizations/group_sort_enum_spec.rb26
-rw-r--r--spec/graphql/types/organizations/organization_type_spec.rb11
-rw-r--r--spec/graphql/types/query_type_spec.rb10
-rw-r--r--spec/models/abuse_report_spec.rb27
-rw-r--r--spec/requests/api/graphql/organizations/organization_query_spec.rb141
-rw-r--r--spec/serializers/admin/abuse_report_details_entity_spec.rb111
-rw-r--r--spec/serializers/admin/abuse_report_details_serializer_spec.rb5
-rw-r--r--spec/serializers/admin/abuse_report_entity_spec.rb4
-rw-r--r--spec/serializers/admin/reported_content_entity_spec.rb43
41 files changed, 945 insertions, 186 deletions
diff --git a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
index 1490d7e64f5..fb6bc38848c 100644
--- a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
@@ -31,6 +31,11 @@ export default {
alert: { ...alertDefaults },
};
},
+ computed: {
+ similarOpenReports() {
+ return this.abuseReport.user?.similarOpenReports || [];
+ },
+ },
methods: {
showAlert(variant, message) {
this.alert.visible = true;
@@ -49,6 +54,7 @@ export default {
<gl-alert v-if="alert.visible" :variant="alert.variant" class="gl-mt-4" @dismiss="closeAlert">{{
alert.message
}}</gl-alert>
+
<report-header
v-if="abuseReport.user"
:user="abuseReport.user"
@@ -56,7 +62,13 @@ export default {
@showAlert="showAlert"
/>
<user-details v-if="abuseReport.user" :user="abuseReport.user" />
- <reported-content :report="abuseReport.report" :reporter="abuseReport.reporter" />
- <history-items :report="abuseReport.report" :reporter="abuseReport.reporter" />
+
+ <reported-content :report="abuseReport.report" data-testid="reported-content" />
+
+ <div v-for="report in similarOpenReports" :key="report.id" data-testid="similar-open-reports">
+ <reported-content :report="report" />
+ </div>
+
+ <history-items :report="abuseReport.report" />
</section>
</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/history_items.vue b/app/assets/javascripts/admin/abuse_report/components/history_items.vue
index 28b66db84a2..619a8bcfe92 100644
--- a/app/assets/javascripts/admin/abuse_report/components/history_items.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/history_items.vue
@@ -16,13 +16,11 @@ export default {
type: Object,
required: true,
},
- reporter: {
- type: Object,
- required: false,
- default: null,
- },
},
computed: {
+ reporter() {
+ return this.report.reporter;
+ },
reporterName() {
return this.reporter?.name || this.$options.i18n.deletedReporter;
},
@@ -35,7 +33,7 @@ export default {
<!-- The styles `issuable-discussion`, `timeline`, `main-notes-list` and `notes` used below
are declared in app/assets/stylesheets/pages/notes.scss -->
<section class="gl-pt-6 issuable-discussion">
- <h2 class="gl-font-size-h1 gl-mt-0 gl-mb-2">{{ $options.i18n.activity }}</h2>
+ <h2 class="gl-font-lg gl-mt-0 gl-mb-2">{{ $options.i18n.activity }}</h2>
<ul class="timeline main-notes-list notes">
<history-item icon="warning">
<div class="gl-display-flex gl-xs-flex-direction-column">
diff --git a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
index f4f0fcac58f..84d6f25ac05 100644
--- a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
@@ -26,11 +26,6 @@ export default {
type: Object,
required: true,
},
- reporter: {
- type: Object,
- required: false,
- default: null,
- },
},
data() {
return {
@@ -38,6 +33,9 @@ export default {
};
},
computed: {
+ reporter() {
+ return this.report.reporter;
+ },
reporterName() {
return this.reporter?.name || this.$options.i18n.deletedReporter;
},
@@ -67,11 +65,12 @@ export default {
<template>
<div class="gl-pt-6">
<div
- class="gl-pb-3 gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column"
+ class="gl-pb-3 gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column gl-align-items-center"
>
- <h2 class="gl-font-size-h1 gl-mt-0 gl-mb-2">
+ <h2 class="gl-font-lg gl-mt-2 gl-mb-2">
{{ $options.i18n.reportTypes[reportType] }}
</h2>
+
<div
class="gl-display-flex gl-align-items-stretch gl-xs-flex-direction-column gl-mt-3 gl-sm-mt-0"
>
diff --git a/app/assets/javascripts/admin/abuse_report/components/user_details.vue b/app/assets/javascripts/admin/abuse_report/components/user_details.vue
index 3dc03a8748f..fe0add1ba8d 100644
--- a/app/assets/javascripts/admin/abuse_report/components/user_details.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/user_details.vue
@@ -39,19 +39,27 @@ export default {
<template>
<div class="gl-mt-6">
- <user-detail data-testid="createdAt" :label="$options.i18n.createdAt">
+ <user-detail data-testid="created-at" :label="$options.i18n.createdAt">
<time-ago-tooltip :time="user.createdAt" />
</user-detail>
+
<user-detail data-testid="email" :label="$options.i18n.email">
<gl-link :href="`mailto:${user.email}`">{{ user.email }}</gl-link>
</user-detail>
+
<user-detail data-testid="plan" :label="$options.i18n.plan" :value="user.plan" />
+
<user-detail
data-testid="verification"
:label="$options.i18n.verification"
:value="verificationState"
/>
- <user-detail v-if="user.creditCard" data-testid="creditCard" :label="$options.i18n.creditCard">
+
+ <user-detail
+ v-if="user.creditCard"
+ data-testid="credit-card-verification"
+ :label="$options.i18n.creditCard"
+ >
<gl-sprintf :message="$options.i18n.registeredWith">
<template #name>{{ user.creditCard.name }}</template>
</gl-sprintf>
@@ -65,17 +73,18 @@ export default {
</template>
</gl-sprintf>
</user-detail>
+
<user-detail
- v-if="user.otherReports.length"
- data-testid="otherReports"
- :label="$options.i18n.otherReports"
+ v-if="user.pastClosedReports.length"
+ data-testid="past-closed-reports"
+ :label="$options.i18n.pastReports"
>
<div
- v-for="(report, index) in user.otherReports"
+ v-for="(report, index) in user.pastClosedReports"
:key="index"
- :data-testid="`other-report-${index}`"
+ :data-testid="`past-report-${index}`"
>
- <gl-sprintf :message="$options.i18n.otherReport">
+ <gl-sprintf :message="$options.i18n.reportedFor">
<template #reportLink="{ content }">
<gl-link :href="report.reportPath">{{ content }}</gl-link>
</template>
@@ -86,28 +95,33 @@ export default {
</gl-sprintf>
</div>
</user-detail>
+
<user-detail
- data-testid="normalLocation"
+ data-testid="normal-location"
:label="$options.i18n.normalLocation"
:value="user.mostUsedIp || user.lastSignInIp"
/>
+
<user-detail
- data-testid="lastSignInIp"
+ data-testid="last-sign-in-ip"
:label="$options.i18n.lastSignInIp"
:value="user.lastSignInIp"
/>
+
<user-detail
- data-testid="snippets"
+ data-testid="user-snippets-count"
:label="$options.i18n.snippets"
:value="$options.i18n.snippetsCount(user.snippetsCount)"
/>
+
<user-detail
- data-testid="groups"
+ data-testid="user-groups-count"
:label="$options.i18n.groups"
:value="$options.i18n.groupsCount(user.groupsCount)"
/>
+
<user-detail
- data-testid="notes"
+ data-testid="user-notes-count"
:label="$options.i18n.notes"
:value="$options.i18n.notesCount(user.notesCount)"
/>
diff --git a/app/assets/javascripts/admin/abuse_report/constants.js b/app/assets/javascripts/admin/abuse_report/constants.js
index b290581598a..6cae6b24f20 100644
--- a/app/assets/javascripts/admin/abuse_report/constants.js
+++ b/app/assets/javascripts/admin/abuse_report/constants.js
@@ -58,7 +58,7 @@ export const USER_DETAILS_I18N = {
plan: s__('AbuseReport|Tier'),
verification: s__('AbuseReport|Verification'),
creditCard: s__('AbuseReport|Credit card'),
- otherReports: s__('AbuseReport|Abuse reports'),
+ pastReports: s__('AbuseReport|Past abuse reports'),
normalLocation: s__('AbuseReport|Normal location'),
lastSignInIp: s__('AbuseReport|Last login'),
snippets: s__('AbuseReport|Snippets'),
@@ -72,7 +72,7 @@ export const USER_DETAILS_I18N = {
phone: s__('AbuseReport|Phone'),
creditCard: s__('AbuseReport|Credit card'),
},
- otherReport: s__(
+ reportedFor: s__(
'AbuseReport|%{reportLinkStart}Reported%{reportLinkEnd} for %{category} %{timeAgo}.',
),
registeredWith: s__('AbuseReport|Registered with name %{name}.'),
diff --git a/app/finders/organizations/groups_finder.rb b/app/finders/organizations/groups_finder.rb
new file mode 100644
index 00000000000..2b59a3106a3
--- /dev/null
+++ b/app/finders/organizations/groups_finder.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+# Organizations::GroupsFinder
+#
+# Used to find Groups within an Organization
+module Organizations
+ class GroupsFinder
+ # @param organization [Organizations::Organization]
+ # @param current_user [User]
+ # @param params [{ sort: { field: [String], direction: [String] }, search: [String] }]
+ def initialize(organization:, current_user:, params: {})
+ @organization = organization
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ return Group.none if organization.nil? || !authorized?
+
+ filter_groups(all_accessible_groups)
+ .then { |groups| sort(groups) }
+ .then(&:with_route)
+ end
+
+ private
+
+ attr_reader :organization, :params, :current_user
+
+ def all_accessible_groups
+ current_user.authorized_groups.in_organization(organization)
+ end
+
+ def filter_groups(groups)
+ by_search(groups)
+ end
+
+ def by_search(groups)
+ return groups unless params[:search].present?
+
+ groups.search(params[:search])
+ end
+
+ def sort(groups)
+ return default_sort_order(groups) if params[:sort].blank?
+
+ field = params[:sort][:field]
+ direction = params[:sort][:direction]
+ groups.reorder(field => direction) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def default_sort_order(groups)
+ groups.sort_by_attribute('name_asc')
+ end
+
+ def authorized?
+ Ability.allowed?(current_user, :read_organization, organization)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/organizations/groups_resolver.rb b/app/graphql/resolvers/organizations/groups_resolver.rb
new file mode 100644
index 00000000000..0f50713b9b4
--- /dev/null
+++ b/app/graphql/resolvers/organizations/groups_resolver.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Organizations
+ class GroupsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include ResolvesGroups
+
+ type Types::GroupType.connection_type, null: true
+
+ authorize :read_group
+
+ argument :search,
+ GraphQL::Types::String,
+ required: false,
+ description: 'Search query for group name or full path.',
+ alpha: { milestone: '16.4' }
+
+ argument :sort,
+ Types::Organizations::GroupSortEnum,
+ description: 'Criteria to sort organization groups by.',
+ required: false,
+ default_value: { field: 'name', direction: :asc },
+ alpha: { milestone: '16.4' }
+
+ private
+
+ def resolve_groups(**args)
+ return Group.none if Feature.disabled?(:resolve_organization_groups, context[:current_user])
+
+ ::Organizations::GroupsFinder
+ .new(organization: object, current_user: context[:current_user], params: args)
+ .execute
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/organizations/organization_resolver.rb b/app/graphql/resolvers/organizations/organization_resolver.rb
new file mode 100644
index 00000000000..9194d9a32c5
--- /dev/null
+++ b/app/graphql/resolvers/organizations/organization_resolver.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Organizations
+ class OrganizationResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ authorize :read_organization
+
+ type Types::Organizations::OrganizationType, null: true
+
+ argument :id,
+ Types::GlobalIDType[::Organizations::Organization],
+ required: true,
+ description: 'ID of the organization.'
+
+ def resolve(id:)
+ authorized_find!(id: id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/organizations/group_sort_enum.rb b/app/graphql/types/organizations/group_sort_enum.rb
new file mode 100644
index 00000000000..8fb2f553539
--- /dev/null
+++ b/app/graphql/types/organizations/group_sort_enum.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Types
+ module Organizations
+ class GroupSortEnum < BaseEnum
+ graphql_name 'OrganizationGroupSort'
+ description 'Values for sorting organization groups'
+
+ sortable_fields = ['ID', 'Name', 'Path', 'Updated at', 'Created at']
+
+ sortable_fields.each do |field|
+ value "#{field.upcase.tr(' ', '_')}_ASC",
+ value: { field: field.downcase.tr(' ', '_'), direction: :asc },
+ description: "#{field} in ascending order.",
+ alpha: { milestone: '16.4' }
+
+ value "#{field.upcase.tr(' ', '_')}_DESC",
+ value: { field: field.downcase.tr(' ', '_'), direction: :desc },
+ description: "#{field} in descending order.",
+ alpha: { milestone: '16.4' }
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/organizations/organization_type.rb b/app/graphql/types/organizations/organization_type.rb
new file mode 100644
index 00000000000..791fddc5266
--- /dev/null
+++ b/app/graphql/types/organizations/organization_type.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Types
+ module Organizations
+ class OrganizationType < BaseObject
+ graphql_name 'Organization'
+
+ authorize :read_organization
+
+ field :groups,
+ Types::GroupType.connection_type,
+ null: false,
+ description: 'Groups within this organization that the user has access to.',
+ alpha: { milestone: '16.4' },
+ resolver: ::Resolvers::Organizations::GroupsResolver
+ field :id,
+ GraphQL::Types::ID,
+ null: false,
+ description: 'ID of the organization.',
+ alpha: { milestone: '16.4' }
+ field :name,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Name of the organization.',
+ alpha: { milestone: '16.4' }
+ field :path,
+ GraphQL::Types::String,
+ null: false,
+ description: 'Path of the organization.',
+ alpha: { milestone: '16.4' }
+ end
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 38b8973034d..e3dd0211029 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -96,6 +96,12 @@ module Types
required: true,
description: 'Global ID of the note.'
end
+ field :organization,
+ Types::Organizations::OrganizationType,
+ null: true,
+ resolver: Resolvers::Organizations::OrganizationResolver,
+ description: "Find an organization.",
+ alpha: { milestone: '16.4' }
field :package,
description: 'Find a package. This field can only be resolved for one query in any single request. Returns `null` if a package has no `default` status.',
resolver: Resolvers::PackageDetailsResolver
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 75c90d370c3..afac53762a7 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -61,8 +61,8 @@ class AbuseReport < ApplicationRecord
validates :screenshot, file_size: { maximum: MAX_FILE_SIZE }
validate :validate_screenshot_is_image
- scope :by_user_id, ->(id) { where(user_id: id) }
- scope :by_reporter_id, ->(id) { where(reporter_id: id) }
+ scope :by_user_id, ->(user_id) { where(user_id: user_id) }
+ scope :by_reporter_id, ->(reporter_id) { where(reporter_id: reporter_id) }
scope :by_category, ->(category) { where(category: category) }
scope :with_users, -> { includes(:reporter, :user) }
@@ -141,8 +141,14 @@ class AbuseReport < ApplicationRecord
end
end
- def other_reports_for_user
- user.abuse_reports.id_not_in(id)
+ def past_closed_reports_for_user
+ user.abuse_reports.closed.id_not_in(id)
+ end
+
+ def similar_open_reports_for_user
+ return AbuseReport.none unless open?
+
+ user.abuse_reports.open.by_category(category).id_not_in(id).includes(:reporter)
end
private
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index a7d03c3688a..be39b894ef9 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -167,6 +167,7 @@ class Namespace < ApplicationRecord
scope :include_route, -> { includes(:route) }
scope :by_parent, -> (parent) { where(parent_id: parent) }
scope :filter_by_path, -> (query) { where('lower(path) = :query', query: query.downcase) }
+ scope :in_organization, -> (organization) { where(organization: organization) }
scope :with_statistics, -> do
joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb
index 9f2119949fb..489fd6e0da7 100644
--- a/app/models/organizations/organization.rb
+++ b/app/models/organizations/organization.rb
@@ -38,7 +38,7 @@ module Organizations
end
def user?(user)
- users.exists?(user.id)
+ organization_users.exists?(user: user)
end
private
diff --git a/app/serializers/admin/abuse_report_details_entity.rb b/app/serializers/admin/abuse_report_details_entity.rb
index 3efb8508e5e..8a67aabda9e 100644
--- a/app/serializers/admin/abuse_report_details_entity.rb
+++ b/app/serializers/admin/abuse_report_details_entity.rb
@@ -8,17 +8,21 @@ module Admin
expose :details, merge: true do |report|
UserEntity.represent(report.user, only: [:name, :username, :avatar_url, :email, :created_at, :last_activity_on])
end
+
expose :path do |report|
user_path(report.user)
end
+
expose :admin_path do |report|
admin_user_path(report.user)
end
+
expose :plan do |report|
if Gitlab::CurrentSettings.current_application_settings.try(:should_check_namespace_plan?)
report.user.namespace&.actual_plan&.title
end
end
+
expose :verification_state do
expose :email do |report|
report.user.confirmed?
@@ -30,6 +34,7 @@ module Admin
report.user.credit_card_validation.present?
end
end
+
expose :credit_card, if: ->(report) { report.user.credit_card_validation&.holder_name } do
expose :name do |report|
report.user.credit_card_validation.holder_name
@@ -41,55 +46,38 @@ module Admin
card_match_admin_user_path(report.user) if Gitlab.ee?
end
end
- expose :other_reports do |report|
- AbuseReportEntity.represent(report.other_reports_for_user, only: [:created_at, :category, :report_path])
+
+ expose :past_closed_reports do |report|
+ AbuseReportEntity.represent(report.past_closed_reports_for_user, only: [:created_at, :category, :report_path])
+ end
+
+ expose :similar_open_reports, if: ->(report) { report.open? } do |report|
+ ReportedContentEntity.represent(report.similar_open_reports_for_user)
end
+
expose :most_used_ip do |report|
AuthenticationEvent.most_used_ip_address_for_user(report.user)
end
+
expose :last_sign_in_ip do |report|
report.user.last_sign_in_ip
end
+
expose :snippets_count do |report|
report.user.snippets.count
end
+
expose :groups_count do |report|
report.user.groups.count
end
+
expose :notes_count do |report|
report.user.notes.count
end
end
- expose :reporter, if: ->(report) { report.reporter } do
- expose :details, merge: true do |report|
- UserEntity.represent(report.reporter, only: [:name, :username, :avatar_url])
- end
- expose :path do |report|
- user_path(report.reporter)
- end
- end
-
- expose :report do
- expose :status
- expose :message
- expose :created_at, as: :reported_at
- expose :category
- expose :report_type, as: :type
- expose :reported_content, as: :content
- expose :reported_from_url, as: :url
- expose :screenshot_path, as: :screenshot
-
- # Kept for backwards compatibility.
- # TODO: See https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/167?work_item_iid=443
- # In 16.4 remove or re-use this field after frontend has migrated to using moderate_user_path
- expose :update_path do |report|
- admin_abuse_report_path(report)
- end
-
- expose :moderate_user_path do |report|
- moderate_user_admin_abuse_report_path(report)
- end
+ expose :report do |report|
+ ReportedContentEntity.represent(report)
end
end
end
diff --git a/app/serializers/admin/reported_content_entity.rb b/app/serializers/admin/reported_content_entity.rb
new file mode 100644
index 00000000000..0e86a1434f8
--- /dev/null
+++ b/app/serializers/admin/reported_content_entity.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Admin
+ class ReportedContentEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id
+ expose :status
+ expose :message
+ expose :created_at, as: :reported_at
+ expose :category
+ expose :report_type, as: :type
+ expose :reported_content, as: :content
+ expose :reported_from_url, as: :url
+ expose :screenshot_path, as: :screenshot
+
+ expose :reporter, if: ->(report) { report.reporter } do
+ expose :details, merge: true do |report|
+ UserEntity.represent(report.reporter, only: [:name, :username, :avatar_url])
+ end
+
+ expose :path do |report|
+ user_path(report.reporter)
+ end
+ end
+
+ # Kept for backwards compatibility.
+ # TODO: See https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/167?work_item_iid=443
+ # In 16.4 remove or re-use this field after frontend has migrated to using moderate_user_path
+ expose :update_path do |report|
+ admin_abuse_report_path(report)
+ end
+
+ expose :moderate_user_path do |report|
+ moderate_user_admin_abuse_report_path(report)
+ end
+ end
+end
diff --git a/config/feature_flags/development/resolve_organization_groups.yml b/config/feature_flags/development/resolve_organization_groups.yml
new file mode 100644
index 00000000000..7a70c8568a6
--- /dev/null
+++ b/config/feature_flags/development/resolve_organization_groups.yml
@@ -0,0 +1,8 @@
+---
+name: resolve_organization_groups
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128733
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/421673
+milestone: '16.3'
+type: development
+group: group::tenant scale
+default_enabled: true
diff --git a/db/migrate/20230807035953_add_index_to_abuse_reports_on_user_id_status_and_category.rb b/db/migrate/20230807035953_add_index_to_abuse_reports_on_user_id_status_and_category.rb
new file mode 100644
index 00000000000..583a5471145
--- /dev/null
+++ b/db/migrate/20230807035953_add_index_to_abuse_reports_on_user_id_status_and_category.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddIndexToAbuseReportsOnUserIdStatusAndCategory < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'idx_abuse_reports_user_id_status_and_category'
+
+ def up
+ add_concurrent_index :abuse_reports, [:user_id, :status, :category], name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :abuse_reports, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230807035953 b/db/schema_migrations/20230807035953
new file mode 100644
index 00000000000..562bb05a5eb
--- /dev/null
+++ b/db/schema_migrations/20230807035953
@@ -0,0 +1 @@
+998695a3c22394e5e08ac61841ac2255e717b0cc5a50baeabb682d91b3f42f28 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 9555b61a16e..20ba9cb2e5a 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -30334,6 +30334,8 @@ CREATE UNIQUE INDEX i_pm_package_versions_on_package_id_and_version ON pm_packag
CREATE UNIQUE INDEX i_pm_packages_purl_type_and_name ON pm_packages USING btree (purl_type, name);
+CREATE INDEX idx_abuse_reports_user_id_status_and_category ON abuse_reports USING btree (user_id, status, category);
+
CREATE INDEX idx_alert_management_alerts_on_created_at_project_id_with_issue ON alert_management_alerts USING btree (created_at, project_id) WHERE (issue_id IS NOT NULL);
CREATE INDEX idx_analytics_devops_adoption_segments_on_namespace_id ON analytics_devops_adoption_segments USING btree (namespace_id);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index f9937c0941c..57fc8dab76a 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -532,6 +532,22 @@ Returns [`Note`](#note).
| ---- | ---- | ----------- |
| <a id="querynoteid"></a>`id` | [`NoteID!`](#noteid) | Global ID of the note. |
+### `Query.organization`
+
+Find an organization.
+
+WARNING:
+**Introduced** in 16.4.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Returns [`Organization`](#organization).
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="queryorganizationid"></a>`id` | [`OrganizationsOrganizationID!`](#organizationsorganizationid) | ID of the organization. |
+
### `Query.package`
Find a package. This field can only be resolved for one query in any single request. Returns `null` if a package has no `default` status.
@@ -20252,6 +20268,39 @@ Active period time range for on-call rotation.
| <a id="oncallrotationactiveperiodtypeendtime"></a>`endTime` | [`String`](#string) | End of the rotation active period. |
| <a id="oncallrotationactiveperiodtypestarttime"></a>`startTime` | [`String`](#string) | Start of the rotation active period. |
+### `Organization`
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="organizationid"></a>`id` **{warning-solid}** | [`ID!`](#id) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. ID of the organization. |
+| <a id="organizationname"></a>`name` **{warning-solid}** | [`String!`](#string) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Name of the organization. |
+| <a id="organizationpath"></a>`path` **{warning-solid}** | [`String!`](#string) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Path of the organization. |
+
+#### Fields with arguments
+
+##### `Organization.groups`
+
+Groups within this organization that the user has access to.
+
+WARNING:
+**Introduced** in 16.4.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Returns [`GroupConnection!`](#groupconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="organizationgroupssearch"></a>`search` **{warning-solid}** | [`String`](#string) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Search query for group name or full path. |
+| <a id="organizationgroupssort"></a>`sort` **{warning-solid}** | [`OrganizationGroupSort`](#organizationgroupsort) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Criteria to sort organization groups by. |
+
### `OrganizationStateCounts`
Represents the total number of organizations for the represented states.
@@ -27246,6 +27295,23 @@ Rotation length unit of an on-call rotation.
| <a id="oncallrotationunitenumhours"></a>`HOURS` | Hours. |
| <a id="oncallrotationunitenumweeks"></a>`WEEKS` | Weeks. |
+### `OrganizationGroupSort`
+
+Values for sorting organization groups.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="organizationgroupsortcreated_at_asc"></a>`CREATED_AT_ASC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Created at in ascending order. |
+| <a id="organizationgroupsortcreated_at_desc"></a>`CREATED_AT_DESC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Created at in descending order. |
+| <a id="organizationgroupsortid_asc"></a>`ID_ASC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. ID in ascending order. |
+| <a id="organizationgroupsortid_desc"></a>`ID_DESC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. ID in descending order. |
+| <a id="organizationgroupsortname_asc"></a>`NAME_ASC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Name in ascending order. |
+| <a id="organizationgroupsortname_desc"></a>`NAME_DESC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Name in descending order. |
+| <a id="organizationgroupsortpath_asc"></a>`PATH_ASC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Path in ascending order. |
+| <a id="organizationgroupsortpath_desc"></a>`PATH_DESC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Path in descending order. |
+| <a id="organizationgroupsortupdated_at_asc"></a>`UPDATED_AT_ASC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Updated at in ascending order. |
+| <a id="organizationgroupsortupdated_at_desc"></a>`UPDATED_AT_DESC` **{warning-solid}** | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Updated at in descending order. |
+
### `OrganizationSort`
Values for sorting organizations.
@@ -28719,6 +28785,12 @@ A `NoteableID` is a global ID. It is encoded as a string.
An example `NoteableID` is: `"gid://gitlab/Noteable/1"`.
+### `OrganizationsOrganizationID`
+
+A `OrganizationsOrganizationID` is a global ID. It is encoded as a string.
+
+An example `OrganizationsOrganizationID` is: `"gid://gitlab/Organizations::Organization/1"`.
+
### `PackagesConanFileMetadatumID`
A `PackagesConanFileMetadatumID` is a global ID. It is encoded as a string.
diff --git a/doc/architecture/blueprints/organization/index.md b/doc/architecture/blueprints/organization/index.md
index 09448d6d90c..ea8f2b75a14 100644
--- a/doc/architecture/blueprints/organization/index.md
+++ b/doc/architecture/blueprints/organization/index.md
@@ -244,6 +244,7 @@ Organizations will have an Owner role. Compared to Users, they can perform the f
| View Groups overview | ✓ | ✓ (1) |
| View Projects overview | ✓ | ✓ (1) |
| View Users overview | ✓ | ✓ (2) |
+| View Organization activity page | ✓ | ✓ (1) |
| Transfer top-level Group into Organization if Owner of both | ✓ | |
(1) Users can only see what they have access to.
@@ -251,13 +252,30 @@ Organizations will have an Owner role. Compared to Users, they can perform the f
[Roles](../../../user/permissions.md) at the Group and Project level remain as they currently are.
+#### Relationship between Organization Owner and Instance Admin
+
+Users with the (Instance) Admin role can currently [administer a self-managed GitLab instance](../../../administration/index.md).
+As functionality is moved to the Organization level, Organization Owners will be able to access more features that are currently only accessible to Admins.
+On our SaaS platform, this helps us in empowering enterprises to manage their own Organization more efficiently without depending on the Instance Admin, which is currently a GitLab team member.
+On SaaS, we expect the Instance Admin and the Organization Owner to be different users.
+Self-managed instances are generally scoped to a single organization, so in this case it is possible that both roles are fulfilled by the same person.
+There are situations that might require intervention by an Instance Admin, for instance when Users are abusing the system.
+When that is the case, actions taken by the Instance Admin overrule actions of the Organization Owner.
+For instance, the Instance Admin can ban or delete a User on behalf of the Organization Owner.
+
### Routing
-Today only Users, Projects, Namespaces and container images are considered routable entities which require global uniqueness on `https://gitlab.com/<path>/-/`. Initially, Organization routes will be [unscoped](../../../development/routing.md). Organizations will follow the path `https://gitlab.com/-/organizations/org-name/` as one of the design goals is that the addition of Organizations should not change existing Group and Project paths.
+Today only Users, Projects, Namespaces and container images are considered routable entities which require global uniqueness on `https://gitlab.com/<path>/-/`.
+Initially, Organization routes will be [unscoped](../../../development/routing.md).
+Organizations will follow the path `https://gitlab.com/-/organizations/org-name/` as one of the design goals is that the addition of Organizations should not change existing Group and Project paths.
### Impact of the Organization on Other Features
-We want a minimal amount of infrequently written tables in the shared database. If we have high write volume or large amounts of data in the shared database then this can become a single bottleneck for scaling and we lose the horizontal scalability objective of Cells. With isolation being one of the main requirements to make Cells work, this means that existing features will mostly be scoped to an Organization rather than work across Organizations. One exception to this are Users, which are stored in the cluster-wide shared database. For a deeper exploration of the impact on select features, see the [list of features impacted by Cells](../cells/index.md#impacted-features).
+We want a minimal amount of infrequently written tables in the shared database.
+If we have high write volume or large amounts of data in the shared database then this can become a single bottleneck for scaling and we lose the horizontal scalability objective of Cells.
+With isolation being one of the main requirements to make Cells work, this means that existing features will mostly be scoped to an Organization rather than work across Organizations.
+One exception to this are Users, which are stored in the cluster-wide shared database.
+For a deeper exploration of the impact on select features, see the [list of features impacted by Cells](../cells/index.md#impacted-features).
## Iteration Plan
@@ -284,6 +302,7 @@ In iteration 2, an Organization MVC Experiment will be released. We will test th
- Users are listed in the User overview. Every Organization User can access the User overview and see Users that are part of the Groups and Projects they have access to.
- Organizations can be deleted.
+- Organization Owners can access the Activity page for the Organization.
- Forking across Organizations will be defined.
### Iteration 3: Organization MVC Beta (FY24Q4)
diff --git a/doc/architecture/blueprints/work_items/index.md b/doc/architecture/blueprints/work_items/index.md
index 9924b0db9f4..6f5b48fffcb 100644
--- a/doc/architecture/blueprints/work_items/index.md
+++ b/doc/architecture/blueprints/work_items/index.md
@@ -46,6 +46,9 @@ Work is underway to convert existing objects to Work Item Types or add new ones:
Every Work Item type has the following common properties:
+**NOTE:**
+You can also refer to fields of [Work Item](../../../api/graphql/reference/index.md#workitem) to learn more.
+
- `id` - a unique Work Item global identifier;
- `iid` - internal ID of the Work Item, relative to the parent workspace (currently workspace can only be a project)
- Work Item type;
@@ -63,20 +66,25 @@ All Work Item types share the same pool of predefined widgets and are customized
### Work Item widget types (updating)
-| widget type | feature flag |
-|---|---|
-| assignees | |
-| description | |
-| hierarchy | |
-| [iteration](https://gitlab.com/gitlab-org/gitlab/-/issues/367456) | |
-| [milestone](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) | |
-| labels | |
-| start and due date | |
-| status\* | |
-| weight | |
-| [notes](https://gitlab.com/gitlab-org/gitlab/-/issues/378949) | |
-
-\* status is not currently a widget, but a part of the root work item, similar to title
+| Widget | Description | feature flag |
+|---|---|---|
+| [WorkItemWidgetAssignees](../../../api/graphql/reference/index.md#workitemwidgetassignees) | List of work item assignees | |
+| [WorkItemWidgetAwardEmoji](../../../api/graphql/reference/index.md#workitemwidgetawardemoji) | Emoji reactions added to work item, including support for upvote/downvote counts | |
+| [WorkItemWidgetCurrentUserTodos](../../../api/graphql/reference/index.md#workitemwidgetcurrentusertodos) | User todo state of work item | |
+| [WorkItemWidgetDescription](../../../api/graphql/reference/index.md#workitemwidgetdescription) | Description of work item, including support for edited state, timestamp, and author | |
+| [WorkItemWidgetHealthStatus](../../../api/graphql/reference/index.md#workitemwidgethealthstatus) | Health status assignment support for work item | |
+| [WorkItemWidgetHierarchy](../../../api/graphql/reference/index.md#workitemwidgethierarchy) | Hierarchy of work items, including support for boolean representing presence of children. **Note:** Hierarchy is currently available only for OKRs. | `okrs_mvc` |
+| [WorkItemWidgetIteration](../../../api/graphql/reference/index.md#workitemwidgetiteration) | Iteration assignment support for work item | |
+| [WorkItemWidgetLabels](../../../api/graphql/reference/index.md#workitemwidgetlabels) | List of labels added to work items, including support for checking whether scoped labels are supported |
+| [WorkItemWidgetLinkedItems](../../../api/graphql/reference/index.md#workitemwidgetlinkeditems) | List of work items added as related to a given work item, with possible relationship types being `relates_to`, `blocks`, and `blocked_by`. Includes support for individual counts of blocked status, blocked by, blocking, and related to. | `linked_work_items` |
+| [WorkItemWidgetMilestone](../../../api/graphql/reference/index.md#workitemwidgetmilestone) | Milestone assignment support for work item | |
+| [WorkItemWidgetNotes](../../../api/graphql/reference/index.md#workitemwidgetnotes) | List of discussions within a work item | |
+| [WorkItemWidgetNotifications](../../../api/graphql/reference/index.md#workitemwidgetnotifications) | Notifications subscription status of a work item for current user | |
+| [WorkItemWidgetProgress](../../../api/graphql/reference/index.md#workitemwidgetprogress) | Progress value of a work item. **Note:** Progress is currently available only for OKRs. | `okrs_mvc` |
+| [WorkItemWidgetStartAndDueDate](../../../api/graphql/reference/index.md#workitemwidgetstartandduedate) | Set start and due dates for a work item | |
+| [WorkItemWidgetStatus](../../../api/graphql/reference/index.md#workitemwidgetstatus) | Status of a work item when type is Requirement, with possible status types being `unverified`, `satisfied`, or `failed` | |
+| [WorkItemWidgetTestReports](../../../api/graphql/reference/index.md#workitemwidgettestreports) | Test reports associated with a work item | |
+| [WorkItemWidgetWeight](../../../api/graphql/reference/index.md#workitemwidgetweight) | Set weight of a work item | |
### Work item relationships
diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
index c1aedbe1111..1eccd5b1b91 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_BUILD_IMAGE_VERSION: 'v1.38.1'
+ AUTO_BUILD_IMAGE_VERSION: 'v1.39.0'
build:
stage: build
diff --git a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
index c1aedbe1111..1eccd5b1b91 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_BUILD_IMAGE_VERSION: 'v1.38.1'
+ AUTO_BUILD_IMAGE_VERSION: 'v1.39.0'
build:
stage: build
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index fcffb6dd420..8b9a98b66ae 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2236,9 +2236,6 @@ msgstr ""
msgid "AbuseReport|%{reportLinkStart}Reported%{reportLinkEnd} for %{category} %{timeAgo}."
msgstr ""
-msgid "AbuseReport|Abuse reports"
-msgstr ""
-
msgid "AbuseReport|Abuse unconfirmed"
msgstr ""
@@ -2353,6 +2350,9 @@ msgstr ""
msgid "AbuseReport|Other"
msgstr ""
+msgid "AbuseReport|Past abuse reports"
+msgstr ""
+
msgid "AbuseReport|Personal information or credentials"
msgstr ""
diff --git a/spec/finders/organizations/groups_finder_spec.rb b/spec/finders/organizations/groups_finder_spec.rb
new file mode 100644
index 00000000000..972d80ab036
--- /dev/null
+++ b/spec/finders/organizations/groups_finder_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Organizations::GroupsFinder, feature_category: :cell do
+ include AdminModeHelper
+
+ let(:current_user) { user }
+ let(:params) { {} }
+ let(:finder) { described_class.new(organization: organization, current_user: current_user, params: params) }
+
+ let_it_be(:organization_user) { create(:organization_user) }
+ let_it_be(:organization) { organization_user.organization }
+ let_it_be(:user) { organization_user.user }
+ let_it_be(:public_group) { create(:group, name: 'public-group', organization: organization) }
+ let_it_be(:other_group) { create(:group, name: 'other-group', organization: organization) }
+ let_it_be(:outside_organization_group) { create(:group) }
+ let_it_be(:private_group) do
+ create(:group, :private, name: 'private-group', organization: organization)
+ end
+
+ let_it_be(:no_access_group_in_org) do
+ create(:group, :private, name: 'no-access', organization: organization)
+ end
+
+ before_all do
+ private_group.add_developer(user)
+ public_group.add_developer(user)
+ other_group.add_developer(user)
+ outside_organization_group.add_developer(user)
+ end
+
+ subject(:result) { finder.execute.to_a }
+
+ describe '#execute' do
+ context 'when user is not authorized to read the organization' do
+ let(:current_user) { create(:user) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when organization is nil' do
+ let(:finder) { described_class.new(organization: nil, current_user: current_user, params: params) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when user is authorized to read the organization' do
+ it 'return all accessible groups' do
+ expect(result).to contain_exactly(public_group, private_group, other_group)
+ end
+
+ context 'when search param is passed' do
+ let(:params) { { search: 'the' } }
+
+ it 'filters the groups by search' do
+ expect(result).to contain_exactly(other_group)
+ end
+ end
+
+ context 'when sort param is not passed' do
+ it 'return groups sorted by name in ascending order by default' do
+ expect(result).to eq([other_group, private_group, public_group])
+ end
+ end
+
+ context 'when sort param is passed' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:field, :direction, :sorted_groups) do
+ 'name' | 'asc' | lazy { [other_group, private_group, public_group] }
+ 'name' | 'desc' | lazy { [public_group, private_group, other_group] }
+ 'path' | 'asc' | lazy { [other_group, private_group, public_group] }
+ 'path' | 'desc' | lazy { [public_group, private_group, other_group] }
+ end
+
+ with_them do
+ let(:params) { { sort: { field: field, direction: direction } } }
+ it 'sorts the groups' do
+ expect(result).to eq(sorted_groups)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
index e519684bbc5..637f035be8c 100644
--- a/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
+++ b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AbuseReportApp from '~/admin/abuse_report/components/abuse_report_app.vue';
import ReportHeader from '~/admin/abuse_report/components/report_header.vue';
import UserDetails from '~/admin/abuse_report/components/user_details.vue';
@@ -14,11 +14,14 @@ describe('AbuseReportApp', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findReportHeader = () => wrapper.findComponent(ReportHeader);
const findUserDetails = () => wrapper.findComponent(UserDetails);
- const findReportedContent = () => wrapper.findComponent(ReportedContent);
+ const findReportedContent = () => wrapper.findByTestId('reported-content');
+ const findSimilarOpenReports = () => wrapper.findAllByTestId('similar-open-reports');
+ const findSimilarReportedContent = () =>
+ findSimilarOpenReports().at(0).findComponent(ReportedContent);
const findHistoryItems = () => wrapper.findComponent(HistoryItems);
const createComponent = (props = {}) => {
- wrapper = shallowMount(AbuseReportApp, {
+ wrapper = shallowMountExtended(AbuseReportApp, {
propsData: {
abuseReport: mockAbuseReport,
...props,
@@ -103,11 +106,16 @@ describe('AbuseReportApp', () => {
it('renders ReportedContent', () => {
expect(findReportedContent().props('report')).toBe(mockAbuseReport.report);
- expect(findReportedContent().props('reporter')).toBe(mockAbuseReport.reporter);
+ });
+
+ it('renders similar abuse reports', () => {
+ const { similarOpenReports } = mockAbuseReport.user;
+
+ expect(findSimilarOpenReports()).toHaveLength(similarOpenReports.length);
+ expect(findSimilarReportedContent().props('report')).toBe(similarOpenReports[0]);
});
it('renders HistoryItems', () => {
expect(findHistoryItems().props('report')).toBe(mockAbuseReport.report);
- expect(findHistoryItems().props('reporter')).toBe(mockAbuseReport.reporter);
});
});
diff --git a/spec/frontend/admin/abuse_report/components/history_items_spec.js b/spec/frontend/admin/abuse_report/components/history_items_spec.js
index 86e994fdc57..e8888810095 100644
--- a/spec/frontend/admin/abuse_report/components/history_items_spec.js
+++ b/spec/frontend/admin/abuse_report/components/history_items_spec.js
@@ -10,7 +10,7 @@ import { mockAbuseReport } from '../mock_data';
describe('HistoryItems', () => {
let wrapper;
- const { report, reporter } = mockAbuseReport;
+ const { report } = mockAbuseReport;
const findHistoryItem = () => wrapper.findComponent(HistoryItem);
const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
@@ -19,7 +19,6 @@ describe('HistoryItems', () => {
wrapper = shallowMount(HistoryItems, {
propsData: {
report,
- reporter,
...props,
},
stubs: {
@@ -39,7 +38,7 @@ describe('HistoryItems', () => {
describe('rendering the title', () => {
it('renders the reporters name and the category', () => {
const title = sprintf(HISTORY_ITEMS_I18N.reportedByForCategory, {
- name: reporter.name,
+ name: report.reporter.name,
category: report.category,
});
expect(findHistoryItem().text()).toContain(title);
@@ -47,7 +46,7 @@ describe('HistoryItems', () => {
describe('when the reporter is not defined', () => {
beforeEach(() => {
- createComponent({ reporter: undefined });
+ createComponent({ report: { ...report, reporter: undefined } });
});
it('renders the `No user found` as the reporters name and the category', () => {
diff --git a/spec/frontend/admin/abuse_report/components/reported_content_spec.js b/spec/frontend/admin/abuse_report/components/reported_content_spec.js
index 9fc49f08f8c..2f16f5a7af2 100644
--- a/spec/frontend/admin/abuse_report/components/reported_content_spec.js
+++ b/spec/frontend/admin/abuse_report/components/reported_content_spec.js
@@ -14,7 +14,7 @@ const modalId = 'abuse-report-screenshot-modal';
describe('ReportedContent', () => {
let wrapper;
- const { report, reporter } = { ...mockAbuseReport };
+ const { report } = { ...mockAbuseReport };
const findScreenshotButton = () => wrapper.findByTestId('screenshot-button');
const findReportUrlButton = () => wrapper.findByTestId('report-url-button');
@@ -32,7 +32,6 @@ describe('ReportedContent', () => {
wrapper = shallowMountExtended(ReportedContent, {
propsData: {
report,
- reporter,
...props,
},
stubs: {
@@ -167,18 +166,18 @@ describe('ReportedContent', () => {
describe('rendering the card footer', () => {
it('renders the reporters avatar', () => {
- expect(findAvatar().props('src')).toBe(reporter.avatarUrl);
+ expect(findAvatar().props('src')).toBe(report.reporter.avatarUrl);
});
it('renders the users name', () => {
- expect(findCardFooter().text()).toContain(reporter.name);
+ expect(findCardFooter().text()).toContain(report.reporter.name);
});
it('renders a link to the users profile page', () => {
const link = findProfileLink();
- expect(link.attributes('href')).toBe(reporter.path);
- expect(link.text()).toBe(`@${reporter.username}`);
+ expect(link.attributes('href')).toBe(report.reporter.path);
+ expect(link.text()).toBe(`@${report.reporter.username}`);
});
it('renders the time-ago tooltip', () => {
diff --git a/spec/frontend/admin/abuse_report/components/user_details_spec.js b/spec/frontend/admin/abuse_report/components/user_details_spec.js
index ca499fbaa6e..f3d8d5bb610 100644
--- a/spec/frontend/admin/abuse_report/components/user_details_spec.js
+++ b/spec/frontend/admin/abuse_report/components/user_details_spec.js
@@ -18,7 +18,7 @@ describe('UserDetails', () => {
const findLinkFor = (attribute) => findLinkIn(findUserDetail(attribute));
const findTimeIn = (component) => component.findComponent(TimeAgoTooltip).props('time');
const findTimeFor = (attribute) => findTimeIn(findUserDetail(attribute));
- const findOtherReport = (index) => wrapper.findByTestId(`other-report-${index}`);
+ const findPastReport = (index) => wrapper.findByTestId(`past-report-${index}`);
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(UserDetails, {
@@ -38,8 +38,8 @@ describe('UserDetails', () => {
describe('createdAt', () => {
it('renders the users createdAt with the correct label', () => {
- expect(findUserDetailLabel('createdAt')).toBe(USER_DETAILS_I18N.createdAt);
- expect(findTimeFor('createdAt')).toBe(user.createdAt);
+ expect(findUserDetailLabel('created-at')).toBe(USER_DETAILS_I18N.createdAt);
+ expect(findTimeFor('created-at')).toBe(user.createdAt);
});
});
@@ -67,32 +67,34 @@ describe('UserDetails', () => {
describe('creditCard', () => {
it('renders the correct label', () => {
- expect(findUserDetailLabel('creditCard')).toBe(USER_DETAILS_I18N.creditCard);
+ expect(findUserDetailLabel('credit-card-verification')).toBe(USER_DETAILS_I18N.creditCard);
});
it('renders the users name', () => {
- expect(findUserDetail('creditCard').text()).toContain(
+ expect(findUserDetail('credit-card-verification').text()).toContain(
sprintf(USER_DETAILS_I18N.registeredWith, { ...user.creditCard }),
);
- expect(findUserDetail('creditCard').text()).toContain(user.creditCard.name);
+ expect(findUserDetail('credit-card-verification').text()).toContain(user.creditCard.name);
});
describe('similar credit cards', () => {
it('renders the number of similar records', () => {
- expect(findUserDetail('creditCard').text()).toContain(
+ expect(findUserDetail('credit-card-verification').text()).toContain(
sprintf('Card matches %{similarRecordsCount} accounts', { ...user.creditCard }),
);
});
it('renders a link to the matching cards', () => {
- expect(findLinkFor('creditCard').attributes('href')).toBe(user.creditCard.cardMatchesLink);
+ expect(findLinkFor('credit-card-verification').attributes('href')).toBe(
+ user.creditCard.cardMatchesLink,
+ );
- expect(findLinkFor('creditCard').text()).toBe(
+ expect(findLinkFor('credit-card-verification').text()).toBe(
sprintf('%{similarRecordsCount} accounts', { ...user.creditCard }),
);
- expect(findLinkFor('creditCard').text()).toContain(
+ expect(findLinkFor('credit-card-verification').text()).toContain(
user.creditCard.similarRecordsCount.toString(),
);
});
@@ -105,13 +107,13 @@ describe('UserDetails', () => {
});
it('does not render the number of similar records', () => {
- expect(findUserDetail('creditCard').text()).not.toContain(
+ expect(findUserDetail('credit-card-verification').text()).not.toContain(
sprintf('Card matches %{similarRecordsCount} accounts', { ...user.creditCard }),
);
});
it('does not render a link to the matching cards', () => {
- expect(findLinkFor('creditCard').exists()).toBe(false);
+ expect(findLinkFor('credit-card-verification').exists()).toBe(false);
});
});
});
@@ -124,55 +126,55 @@ describe('UserDetails', () => {
});
it('does not render the users creditCard', () => {
- expect(findUserDetail('creditCard').exists()).toBe(false);
+ expect(findUserDetail('credit-card-verification').exists()).toBe(false);
});
});
});
describe('otherReports', () => {
it('renders the correct label', () => {
- expect(findUserDetailLabel('otherReports')).toBe(USER_DETAILS_I18N.otherReports);
+ expect(findUserDetailLabel('past-closed-reports')).toBe(USER_DETAILS_I18N.pastReports);
});
- describe.each(user.otherReports)('renders a line for report %#', (otherReport) => {
- const index = user.otherReports.indexOf(otherReport);
+ describe.each(user.pastClosedReports)('renders a line for report %#', (pastReport) => {
+ const index = user.pastClosedReports.indexOf(pastReport);
it('renders the category', () => {
- expect(findOtherReport(index).text()).toContain(
- sprintf('Reported for %{category}', { ...otherReport }),
+ expect(findPastReport(index).text()).toContain(
+ sprintf('Reported for %{category}', { ...pastReport }),
);
});
it('renders a link to the report', () => {
- expect(findLinkIn(findOtherReport(index)).attributes('href')).toBe(otherReport.reportPath);
+ expect(findLinkIn(findPastReport(index)).attributes('href')).toBe(pastReport.reportPath);
});
it('renders the time it was created', () => {
- expect(findTimeIn(findOtherReport(index))).toBe(otherReport.createdAt);
+ expect(findTimeIn(findPastReport(index))).toBe(pastReport.createdAt);
});
});
describe('when the users otherReports is empty', () => {
beforeEach(() => {
createComponent({
- user: { ...user, otherReports: [] },
+ user: { ...user, pastClosedReports: [] },
});
});
it('does not render the users otherReports', () => {
- expect(findUserDetail('otherReports').exists()).toBe(false);
+ expect(findUserDetail('past-closed-reports').exists()).toBe(false);
});
});
});
describe('normalLocation', () => {
it('renders the correct label', () => {
- expect(findUserDetailLabel('normalLocation')).toBe(USER_DETAILS_I18N.normalLocation);
+ expect(findUserDetailLabel('normal-location')).toBe(USER_DETAILS_I18N.normalLocation);
});
describe('when the users mostUsedIp is blank', () => {
it('renders the users lastSignInIp', () => {
- expect(findUserDetailValue('normalLocation')).toBe(user.lastSignInIp);
+ expect(findUserDetailValue('normal-location')).toBe(user.lastSignInIp);
});
});
@@ -186,23 +188,25 @@ describe('UserDetails', () => {
});
it('renders the users mostUsedIp', () => {
- expect(findUserDetailValue('normalLocation')).toBe(mostUsedIp);
+ expect(findUserDetailValue('normal-location')).toBe(mostUsedIp);
});
});
});
describe('lastSignInIp', () => {
it('renders the users lastSignInIp with the correct label', () => {
- expect(findUserDetailLabel('lastSignInIp')).toBe(USER_DETAILS_I18N.lastSignInIp);
- expect(findUserDetailValue('lastSignInIp')).toBe(user.lastSignInIp);
+ expect(findUserDetailLabel('last-sign-in-ip')).toBe(USER_DETAILS_I18N.lastSignInIp);
+ expect(findUserDetailValue('last-sign-in-ip')).toBe(user.lastSignInIp);
});
});
it.each(['snippets', 'groups', 'notes'])(
'renders the users %s with the correct label',
(attribute) => {
- expect(findUserDetailLabel(attribute)).toBe(USER_DETAILS_I18N[attribute]);
- expect(findUserDetailValue(attribute)).toBe(
+ const testId = `user-${attribute}-count`;
+
+ expect(findUserDetailLabel(testId)).toBe(USER_DETAILS_I18N[attribute]);
+ expect(findUserDetailValue(testId)).toBe(
USER_DETAILS_I18N[`${attribute}Count`](user[`${attribute}Count`]),
);
},
diff --git a/spec/frontend/admin/abuse_report/mock_data.js b/spec/frontend/admin/abuse_report/mock_data.js
index 8ff0c7d507a..4c3f6e7b5ea 100644
--- a/spec/frontend/admin/abuse_report/mock_data.js
+++ b/spec/frontend/admin/abuse_report/mock_data.js
@@ -15,7 +15,7 @@ export const mockAbuseReport = {
similarRecordsCount: 2,
cardMatchesLink: '/admin/users/spamuser417/card_match',
},
- otherReports: [
+ pastClosedReports: [
{
category: 'offensive',
createdAt: '2023-02-28T10:09:54.982Z',
@@ -32,12 +32,24 @@ export const mockAbuseReport = {
snippetsCount: 0,
groupsCount: 0,
notesCount: 6,
- },
- reporter: {
- username: 'reporter',
- name: 'R Porter',
- avatarUrl: 'https://www.gravatar.com/avatar/a2579caffc69ea5d7606f9dd9d8504ba?s=80&d=identicon',
- path: '/reporter',
+ similarOpenReports: [
+ {
+ status: 'open',
+ message: 'This is obvious spam',
+ reportedAt: '2023-03-29T09:39:50.502Z',
+ category: 'spam',
+ type: 'issue',
+ content: '',
+ screenshot: null,
+ reporter: {
+ username: 'reporter 2',
+ name: 'Another Reporter',
+ avatarUrl: 'https://www.gravatar.com/avatar/anotherreporter',
+ path: '/reporter-2',
+ },
+ updatePath: '/admin/abuse_reports/28',
+ },
+ ],
},
report: {
status: 'open',
@@ -52,5 +64,12 @@ export const mockAbuseReport = {
'/uploads/-/system/abuse_report/screenshot/27/Screenshot_2023-03-30_at_16.56.37.png',
updatePath: '/admin/abuse_reports/27',
moderateUserPath: '/admin/abuse_reports/27/moderate_user',
+ reporter: {
+ username: 'reporter',
+ name: 'R Porter',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/a2579caffc69ea5d7606f9dd9d8504ba?s=80&d=identicon',
+ path: '/reporter',
+ },
},
};
diff --git a/spec/graphql/types/organizations/group_sort_enum_spec.rb b/spec/graphql/types/organizations/group_sort_enum_spec.rb
new file mode 100644
index 00000000000..57915d95c45
--- /dev/null
+++ b/spec/graphql/types/organizations/group_sort_enum_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['OrganizationGroupSort'], feature_category: :cell do
+ let(:sort_values) do
+ %w[
+ ID_ASC
+ ID_DESC
+ NAME_ASC
+ NAME_DESC
+ PATH_ASC
+ PATH_DESC
+ UPDATED_AT_ASC
+ UPDATED_AT_DESC
+ CREATED_AT_ASC
+ CREATED_AT_DESC
+ ]
+ end
+
+ specify { expect(described_class.graphql_name).to eq('OrganizationGroupSort') }
+
+ it 'exposes all the organization groups sort values' do
+ expect(described_class.values.keys).to include(*sort_values)
+ end
+end
diff --git a/spec/graphql/types/organizations/organization_type_spec.rb b/spec/graphql/types/organizations/organization_type_spec.rb
new file mode 100644
index 00000000000..78c5dea7308
--- /dev/null
+++ b/spec/graphql/types/organizations/organization_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['Organization'], feature_category: :cell do
+ let(:expected_fields) { %w[groups id name path] }
+
+ specify { expect(described_class.graphql_name).to eq('Organization') }
+ specify { expect(described_class).to require_graphql_authorizations(:read_organization) }
+ specify { expect(described_class).to have_graphql_fields(*expected_fields) }
+end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index 100ecc94f35..8bda738751d 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -23,6 +23,16 @@ RSpec.describe GitlabSchema.types['Query'], feature_category: :shared do
end
end
+ describe 'organization field' do
+ subject { described_class.fields['organization'] }
+
+ it 'finds organization by path' do
+ is_expected.to have_graphql_arguments(:id)
+ is_expected.to have_graphql_type(Types::Organizations::OrganizationType)
+ is_expected.to have_graphql_resolver(Resolvers::Organizations::OrganizationResolver)
+ end
+ end
+
describe 'project field' do
subject { described_class.fields['project'] }
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 584f9b010ad..1fa60a210e2 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -385,13 +385,28 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
end
end
- describe '#other_reports_for_user' do
- let(:report) { create(:abuse_report) }
- let(:another_user_report) { create(:abuse_report, user: report.user) }
- let(:another_report) { create(:abuse_report) }
+ describe '#past_closed_reports_for_user' do
+ let(:report_1) { create(:abuse_report, :closed) }
+ let(:report_2) { create(:abuse_report, user: report.user) }
+ let(:report_3) { create(:abuse_report, :closed, user: report.user) }
- it 'returns other reports for the same user' do
- expect(report.other_reports_for_user).to match_array(another_user_report)
+ it 'returns past closed reports for the same user' do
+ expect(report.past_closed_reports_for_user).to match_array(report_3)
+ end
+ end
+
+ describe '#similar_open_reports_for_user' do
+ let(:report_1) { create(:abuse_report, category: 'spam') }
+ let(:report_2) { create(:abuse_report, category: 'spam', user: report.user) }
+ let(:report_3) { create(:abuse_report, category: 'offensive', user: report.user) }
+ let(:report_4) { create(:abuse_report, :closed, category: 'spam', user: report.user) }
+
+ it 'returns open reports for the same user and category' do
+ expect(report.similar_open_reports_for_user).to match_array(report_2)
+ end
+
+ it 'returns no abuse reports when the report is closed' do
+ expect(report_4.similar_open_reports_for_user).to match_array(described_class.none)
end
end
diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb
new file mode 100644
index 00000000000..2d09a535279
--- /dev/null
+++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting organization information', feature_category: :cell do
+ include GraphqlHelpers
+
+ let(:query) { graphql_query_for(:organization, { id: organization.to_global_id }, organization_fields) }
+ let(:current_user) { user }
+ let(:groups) { graphql_data_at(:organization, :groups, :edges, :node) }
+ let(:organization_fields) do
+ <<~FIELDS
+ id
+ path
+ groups {
+ edges {
+ node {
+ id
+ }
+ }
+ }
+ FIELDS
+ end
+
+ let_it_be(:organization_user) { create(:organization_user) }
+ let_it_be(:organization) { organization_user.organization }
+ let_it_be(:user) { organization_user.user }
+ let_it_be(:public_group) { create(:group, name: 'public-group', organization: organization) }
+ let_it_be(:other_group) { create(:group, name: 'other-group', organization: organization) }
+ let_it_be(:outside_organization_group) { create(:group) }
+
+ let_it_be(:private_group) do
+ create(:group, :private, name: 'private-group', organization: organization)
+ end
+
+ let_it_be(:no_access_group_in_org) do
+ create(:group, :private, name: 'no-access', organization: organization)
+ end
+
+ before_all do
+ private_group.add_developer(user)
+ public_group.add_developer(user)
+ other_group.add_developer(user)
+ outside_organization_group.add_developer(user)
+ end
+
+ subject(:request_organization) { post_graphql(query, current_user: current_user) }
+
+ context 'when the user does not have access to the organization' do
+ let(:current_user) { create(:user) }
+
+ it 'returns the organization as all organizations are public' do
+ request_organization
+
+ expect(graphql_data['organization']['id']).to eq(organization.to_global_id.to_s)
+ end
+ end
+
+ context 'when user has access to the organization' do
+ it_behaves_like 'a working graphql query' do
+ before do
+ request_organization
+ end
+ end
+
+ context 'when resolve_organization_groups feature flag is disabled' do
+ before do
+ stub_feature_flags(resolve_organization_groups: false)
+ end
+
+ it 'returns no groups' do
+ request_organization
+
+ expect(graphql_data['organization']).not_to be_nil
+ expect(graphql_data['organization']['groups']['edges']).to be_empty
+ end
+ end
+
+ context 'with `search` argument' do
+ let(:search) { 'oth' }
+ let(:organization_fields) do
+ <<~FIELDS
+ id
+ path
+ groups(search: "#{search}") {
+ edges {
+ node {
+ id
+ name
+ }
+ }
+ }
+ FIELDS
+ end
+
+ it 'filters groups by name' do
+ request_organization
+
+ expect(groups).to contain_exactly(a_graphql_entity_for(other_group))
+ end
+ end
+
+ context 'with `sort` argument' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:authorized_groups) { [public_group, private_group, other_group] }
+
+ where(:field, :direction, :sorted_groups) do
+ 'id' | 'asc' | lazy { authorized_groups.sort_by(&:id) }
+ 'id' | 'desc' | lazy { authorized_groups.sort_by(&:id).reverse }
+ 'name' | 'asc' | lazy { authorized_groups.sort_by(&:name) }
+ 'name' | 'desc' | lazy { authorized_groups.sort_by(&:name).reverse }
+ 'path' | 'asc' | lazy { authorized_groups.sort_by(&:path) }
+ 'path' | 'desc' | lazy { authorized_groups.sort_by(&:path).reverse }
+ end
+
+ with_them do
+ let(:sort) { "#{field}_#{direction}".upcase }
+ let(:organization_fields) do
+ <<~FIELDS
+ id
+ path
+ groups(sort: #{sort}) {
+ edges {
+ node {
+ id
+ }
+ }
+ }
+ FIELDS
+ end
+
+ it 'sorts the groups' do
+ request_organization
+
+ expect(groups.pluck('id')).to eq(sorted_groups.map(&:to_global_id).map(&:to_s))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/serializers/admin/abuse_report_details_entity_spec.rb b/spec/serializers/admin/abuse_report_details_entity_spec.rb
index 727716d76a4..230cdc88fb9 100644
--- a/spec/serializers/admin/abuse_report_details_entity_spec.rb
+++ b/spec/serializers/admin/abuse_report_details_entity_spec.rb
@@ -5,10 +5,11 @@ require 'spec_helper'
RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threat do
include Gitlab::Routing
- let(:report) { build_stubbed(:abuse_report) }
- let(:user) { report.user }
- let(:reporter) { report.reporter }
- let!(:other_report) { create(:abuse_report, user: user) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
+ let_it_be(:report) { build_stubbed(:abuse_report) }
+ let_it_be(:user) { report.user }
+ let_it_be(:reporter) { report.reporter }
+ let_it_be(:past_report) { create_default(:abuse_report, :closed, user: user) }
+ let_it_be(:similar_open_report) { create_default(:abuse_report, user: user, category: report.category) }
let(:entity) do
described_class.new(report)
@@ -18,11 +19,10 @@ RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threa
subject(:entity_hash) { entity.as_json }
it 'exposes correct attributes' do
- expect(entity_hash.keys).to include(
+ expect(entity_hash.keys).to match_array([
:user,
- :reporter,
:report
- )
+ ])
end
it 'correctly exposes `user`', :aggregate_failures do
@@ -39,7 +39,8 @@ RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threa
:admin_path,
:plan,
:verification_state,
- :other_reports,
+ :past_closed_reports,
+ :similar_open_reports,
:most_used_ip,
:last_sign_in_ip,
:snippets_count,
@@ -53,11 +54,75 @@ RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threa
:credit_card
])
- expect(user_hash[:other_reports][0].keys).to match_array([
+ expect(user_hash[:past_closed_reports][0].keys).to match_array([
:created_at,
:category,
:report_path
])
+
+ similar_open_report_hash = user_hash[:similar_open_reports][0]
+ expect(similar_open_report_hash.keys).to match_array([
+ :id,
+ :status,
+ :message,
+ :reported_at,
+ :category,
+ :type,
+ :content,
+ :url,
+ :screenshot,
+ :update_path,
+ :moderate_user_path,
+ :reporter
+ ])
+
+ similar_reporter_hash = similar_open_report_hash[:reporter]
+ expect(similar_reporter_hash.keys).to match_array([
+ :name,
+ :username,
+ :avatar_url,
+ :path
+ ])
+ end
+
+ context 'when report is closed' do
+ let(:report) { build_stubbed(:abuse_report, :closed) }
+
+ it 'does not expose `user.similar_open_reports`' do
+ user_hash = entity_hash[:user]
+
+ expect(user_hash).not_to include(:similar_open_reports)
+ end
+ end
+
+ it 'correctly exposes `report`', :aggregate_failures do
+ report_hash = entity_hash[:report]
+
+ expect(report_hash.keys).to match_array([
+ :id,
+ :status,
+ :message,
+ :reported_at,
+ :category,
+ :type,
+ :content,
+ :url,
+ :screenshot,
+ :update_path,
+ :moderate_user_path,
+ :reporter
+ ])
+ end
+
+ it 'correctly exposes `reporter`' do
+ reporter_hash = entity_hash[:report][:reporter]
+
+ expect(reporter_hash.keys).to match_array([
+ :name,
+ :username,
+ :avatar_url,
+ :path
+ ])
end
describe 'users plan' do
@@ -110,33 +175,5 @@ RSpec.describe Admin::AbuseReportDetailsEntity, feature_category: :insider_threa
end
end
end
-
- it 'correctly exposes `reporter`' do
- reporter_hash = entity_hash[:reporter]
-
- expect(reporter_hash.keys).to match_array([
- :name,
- :username,
- :avatar_url,
- :path
- ])
- end
-
- it 'correctly exposes `report`' do
- report_hash = entity_hash[:report]
-
- expect(report_hash.keys).to match_array([
- :status,
- :message,
- :reported_at,
- :category,
- :type,
- :content,
- :url,
- :screenshot,
- :update_path,
- :moderate_user_path
- ])
- end
end
end
diff --git a/spec/serializers/admin/abuse_report_details_serializer_spec.rb b/spec/serializers/admin/abuse_report_details_serializer_spec.rb
index a42c56c0921..3bdd2e46ba3 100644
--- a/spec/serializers/admin/abuse_report_details_serializer_spec.rb
+++ b/spec/serializers/admin/abuse_report_details_serializer_spec.rb
@@ -9,11 +9,10 @@ RSpec.describe Admin::AbuseReportDetailsSerializer, feature_category: :insider_t
describe '#represent' do
it 'serializes an abuse report' do
- is_expected.to include(
+ is_expected.to match_array([
:user,
- :reporter,
:report
- )
+ ])
end
end
end
diff --git a/spec/serializers/admin/abuse_report_entity_spec.rb b/spec/serializers/admin/abuse_report_entity_spec.rb
index c7f57258f40..56aa9a46597 100644
--- a/spec/serializers/admin/abuse_report_entity_spec.rb
+++ b/spec/serializers/admin/abuse_report_entity_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Admin::AbuseReportEntity, feature_category: :insider_threat do
subject(:entity_hash) { entity.as_json }
it 'exposes correct attributes' do
- expect(entity_hash.keys).to include(
+ expect(entity_hash.keys).to match_array([
:category,
:created_at,
:updated_at,
@@ -23,7 +23,7 @@ RSpec.describe Admin::AbuseReportEntity, feature_category: :insider_threat do
:reported_user,
:reporter,
:report_path
- )
+ ])
end
it 'correctly exposes `reported user`' do
diff --git a/spec/serializers/admin/reported_content_entity_spec.rb b/spec/serializers/admin/reported_content_entity_spec.rb
new file mode 100644
index 00000000000..21ce5cf64af
--- /dev/null
+++ b/spec/serializers/admin/reported_content_entity_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::ReportedContentEntity, feature_category: :insider_threat do
+ let_it_be(:report) { build_stubbed(:abuse_report) }
+
+ let(:entity) do
+ described_class.new(report)
+ end
+
+ describe '#as_json' do
+ subject(:entity_hash) { entity.as_json }
+
+ it 'exposes correct attributes' do
+ expect(entity_hash.keys).to match_array([
+ :id,
+ :status,
+ :message,
+ :reported_at,
+ :category,
+ :type,
+ :content,
+ :url,
+ :screenshot,
+ :reporter,
+ :update_path,
+ :moderate_user_path
+ ])
+ end
+
+ it 'correctly exposes `reporter`' do
+ reporter_hash = entity_hash[:reporter]
+
+ expect(reporter_hash.keys).to match_array([
+ :name,
+ :username,
+ :avatar_url,
+ :path
+ ])
+ end
+ end
+end