Commit 0d4d35bb authored by Robert Hunt's avatar Robert Hunt Committed by Illya Klymov

Added new AuditEventsApp to group audit events

Created new index.js to load the app and updated the HAML.

Updated the Rails helper and controller to pass the correct info.

Added new changelog

Fixed group audit event specs
parent 0662980b
...@@ -16,6 +16,7 @@ class AuditEvent < ApplicationRecord ...@@ -16,6 +16,7 @@ class AuditEvent < ApplicationRecord
scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) } scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) }
scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) } scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) }
scope :by_author_id, -> (author_id) { where(author_id: author_id) }
after_initialize :initialize_details after_initialize :initialize_details
......
...@@ -23,7 +23,7 @@ export default { ...@@ -23,7 +23,7 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
enabledTokenTypes: { filterTokenOptions: {
type: Array, type: Array,
required: true, required: true,
}, },
...@@ -53,7 +53,7 @@ export default { ...@@ -53,7 +53,7 @@ export default {
<div class="d-flex justify-content-between audit-controls row"> <div class="d-flex justify-content-between audit-controls row">
<div class="col-lg-auto flex-fill form-group align-items-lg-center pr-lg-8"> <div class="col-lg-auto flex-fill form-group align-items-lg-center pr-lg-8">
<audit-events-filter <audit-events-filter
:enabled-token-types="enabledTokenTypes" :filter-token-options="filterTokenOptions"
:qa-selector="filterQaSelector" :qa-selector="filterQaSelector"
:value="filterValue" :value="filterValue"
@selected="setFilterValue" @selected="setFilterValue"
......
<script> <script>
import { GlFilteredSearch } from '@gitlab/ui'; import { GlFilteredSearch } from '@gitlab/ui';
import { FILTER_TOKENS, AVAILABLE_TOKEN_TYPES } from '../constants'; import { AUDIT_FILTER_CONFIGS } from '../constants';
import { availableTokensValidator } from '../validators'; import { filterTokenOptionsValidator } from '../validators';
export default { export default {
components: { components: {
...@@ -13,11 +13,11 @@ export default { ...@@ -13,11 +13,11 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
enabledTokenTypes: { filterTokenOptions: {
type: Array, type: Array,
required: false, required: false,
default: () => AVAILABLE_TOKEN_TYPES, default: () => AUDIT_FILTER_CONFIGS,
validator: availableTokensValidator, validator: filterTokenOptionsValidator,
}, },
qaSelector: { qaSelector: {
type: String, type: String,
...@@ -25,25 +25,30 @@ export default { ...@@ -25,25 +25,30 @@ export default {
default: undefined, default: undefined,
}, },
}, },
data() {
return {
filterTokens: this.filterTokenOptions.map(option => ({
...AUDIT_FILTER_CONFIGS.find(({ type }) => type === option.type),
...option,
})),
};
},
computed: { computed: {
searchTerm() { tokenSearchTerm() {
return this.value.find(term => AVAILABLE_TOKEN_TYPES.includes(term.type)); return this.value.find(term => this.filterTokens.find(token => token.type === term.type));
}, },
enabledTokens() { enabledTokens() {
return FILTER_TOKENS.filter(token => this.enabledTokenTypes.includes(token.type)); const { tokenSearchTerm } = this;
},
filterTokens() {
// This limits the user to search by only one of the available tokens
const { enabledTokens, searchTerm } = this;
if (searchTerm?.type) { // If a user has searched for a term within a token, limit the user to that one token
return enabledTokens.map(token => ({ if (tokenSearchTerm) {
return this.filterTokens.map(token => ({
...token, ...token,
disabled: searchTerm.type !== token.type, disabled: tokenSearchTerm.type !== token.type,
})); }));
} }
return enabledTokens; return this.filterTokens;
}, },
}, },
methods: { methods: {
...@@ -68,7 +73,7 @@ export default { ...@@ -68,7 +73,7 @@ export default {
:placeholder="__('Search')" :placeholder="__('Search')"
:clear-button-title="__('Clear')" :clear-button-title="__('Clear')"
:close-button-title="__('Close')" :close-button-title="__('Close')"
:available-tokens="filterTokens" :available-tokens="enabledTokens"
class="gl-h-32 w-100" class="gl-h-32 w-100"
@submit="onSubmit" @submit="onSubmit"
@input="onInput" @input="onInput"
......
<script>
import Api from '~/api';
import AuditFilterToken from './shared/audit_filter_token.vue';
export default {
components: {
AuditFilterToken,
},
inheritAttrs: false,
tokenMethods: {
fetchItem(id) {
return Api.user(id).then(res => res.data);
},
fetchSuggestions(term) {
return Api.groupMembers(this.config.groupId, { search: term }).then(res => res.data);
},
getItemName({ name }) {
return name;
},
},
};
</script>
<template>
<audit-filter-token v-bind="{ ...this.$attrs, ...this.$options.tokenMethods }" v-on="$listeners">
<template #suggestion="{item: user}">
<p class="m-0">{{ user.name }}</p>
<p class="m-0">@{{ user.username }}</p>
</template>
</audit-filter-token>
</template>
...@@ -64,7 +64,10 @@ export default { ...@@ -64,7 +64,10 @@ export default {
return this.suggestions.length > 0; return this.suggestions.length > 0;
}, },
lowerCaseType() { lowerCaseType() {
return this.config.type.toLowerCase(); return this.config.type
.replace('_', ' ')
.trim()
.toLowerCase();
}, },
noSuggestionsString() { noSuggestionsString() {
return sprintf(s__('AuditLogs|No matching %{type} found.'), { type: this.lowerCaseType }); return sprintf(s__('AuditLogs|No matching %{type} found.'), { type: this.lowerCaseType });
......
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import UserToken from './components/tokens/user_token.vue'; import UserToken from './components/tokens/user_token.vue';
import GroupMemberToken from './components/tokens/group_member_token.vue';
import ProjectToken from './components/tokens/project_token.vue'; import ProjectToken from './components/tokens/project_token.vue';
import GroupToken from './components/tokens/group_token.vue'; import GroupToken from './components/tokens/group_token.vue';
...@@ -8,28 +10,49 @@ const DEFAULT_TOKEN_OPTIONS = { ...@@ -8,28 +10,49 @@ const DEFAULT_TOKEN_OPTIONS = {
unique: true, unique: true,
}; };
export const FILTER_TOKENS = [ // Due to the i18n eslint rule we can't have a capitalized string even if it is a case-aware URL param
/* eslint-disable @gitlab/require-i18n-strings */
const ENTITY_TYPES = {
USER: 'User',
AUTHOR: 'Author',
GROUP: 'Group',
PROJECT: 'Project',
};
/* eslint-enable @gitlab/require-i18n-strings */
export const AUDIT_FILTER_CONFIGS = [
{ {
...DEFAULT_TOKEN_OPTIONS, ...DEFAULT_TOKEN_OPTIONS,
icon: 'user', icon: 'user',
title: s__('AuditLogs|User Events'), title: s__('AuditLogs|User Events'),
type: 'User', type: 'user',
entityType: ENTITY_TYPES.USER,
token: UserToken, token: UserToken,
}, },
{
...DEFAULT_TOKEN_OPTIONS,
icon: 'user',
title: s__('AuditLogs|Member Events'),
type: 'group_member',
entityType: ENTITY_TYPES.AUTHOR,
token: GroupMemberToken,
},
{ {
...DEFAULT_TOKEN_OPTIONS, ...DEFAULT_TOKEN_OPTIONS,
icon: 'bookmark', icon: 'bookmark',
title: s__('AuditLogs|Project Events'), title: s__('AuditLogs|Project Events'),
type: 'Project', type: 'project',
entityType: ENTITY_TYPES.PROJECT,
token: ProjectToken, token: ProjectToken,
}, },
{ {
...DEFAULT_TOKEN_OPTIONS, ...DEFAULT_TOKEN_OPTIONS,
icon: 'group', icon: 'group',
title: s__('AuditLogs|Group Events'), title: s__('AuditLogs|Group Events'),
type: 'Group', type: 'group',
entityType: ENTITY_TYPES.GROUP,
token: GroupToken, token: GroupToken,
}, },
]; ];
export const AVAILABLE_TOKEN_TYPES = FILTER_TOKENS.map(token => token.type); export const AVAILABLE_TOKEN_TYPES = AUDIT_FILTER_CONFIGS.map(token => token.type);
import Vue from 'vue'; import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import AuditEventsApp from './components/audit_events_app.vue'; import AuditEventsApp from './components/audit_events_app.vue';
import createStore from './store'; import createStore from './store';
export default selector => { export default selector => {
const el = document.querySelector(selector); const el = document.querySelector(selector);
const { events, isLastPage, enabledTokenTypes, filterQaSelector, tableQaSelector } = el.dataset; const { events, isLastPage, filterTokenOptions, filterQaSelector, tableQaSelector } = el.dataset;
const store = createStore(); const store = createStore();
store.dispatch('initializeAuditEvents'); store.dispatch('initializeAuditEvents');
...@@ -18,7 +20,9 @@ export default selector => { ...@@ -18,7 +20,9 @@ export default selector => {
props: { props: {
events: JSON.parse(events), events: JSON.parse(events),
isLastPage: parseBoolean(isLastPage), isLastPage: parseBoolean(isLastPage),
enabledTokenTypes: JSON.parse(enabledTokenTypes), filterTokenOptions: JSON.parse(filterTokenOptions).map(filterTokenOption =>
convertObjectPropsToCamelCase(filterTokenOption),
),
filterQaSelector, filterQaSelector,
tableQaSelector, tableQaSelector,
}, },
......
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility'; import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
import { AVAILABLE_TOKEN_TYPES } from './constants'; import { AVAILABLE_TOKEN_TYPES, AUDIT_FILTER_CONFIGS } from './constants';
export const isNumeric = str => { export const isNumeric = str => {
return !Number.isNaN(parseInt(str, 10), 10); return !Number.isNaN(parseInt(str, 10));
};
export const getTypeFromEntityType = entityType => {
return AUDIT_FILTER_CONFIGS.find(
({ entityType: configEntityType }) => configEntityType === entityType,
)?.type;
};
export const getEntityTypeFromType = type => {
return AUDIT_FILTER_CONFIGS.find(({ type: configType }) => configType === type)?.entityType;
}; };
export const parseAuditEventSearchQuery = ({ export const parseAuditEventSearchQuery = ({
created_after: createdAfter, created_after: createdAfter,
created_before: createdBefore, created_before: createdBefore,
entity_type: entityType,
...restOfParams ...restOfParams
}) => ({ }) => ({
...restOfParams, ...restOfParams,
created_after: createdAfter ? parsePikadayDate(createdAfter) : null, created_after: createdAfter ? parsePikadayDate(createdAfter) : null,
created_before: createdBefore ? parsePikadayDate(createdBefore) : null, created_before: createdBefore ? parsePikadayDate(createdBefore) : null,
entity_type: getTypeFromEntityType(entityType),
}); });
export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, sortBy }) => { export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, sortBy }) => {
...@@ -23,7 +35,7 @@ export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, s ...@@ -23,7 +35,7 @@ export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, s
created_before: endDate ? pikadayToString(endDate) : null, created_before: endDate ? pikadayToString(endDate) : null,
sort: sortBy, sort: sortBy,
entity_id: entityValue?.value.data, entity_id: entityValue?.value.data,
entity_type: entityValue?.type, entity_type: getEntityTypeFromType(entityValue?.type),
// When changing the search parameters, we should be resetting to the first page // When changing the search parameters, we should be resetting to the first page
page: null, page: null,
}; };
......
import { AVAILABLE_TOKEN_TYPES } from './constants'; import { AVAILABLE_TOKEN_TYPES } from './constants';
export function availableTokensValidator(value) { export function filterTokenOptionsValidator(filterTokenOptions) {
return value.every(type => AVAILABLE_TOKEN_TYPES.includes(type)); return filterTokenOptions
.map(({ type }) => type)
.every(type => AVAILABLE_TOKEN_TYPES.includes(type));
} }
export default {}; export default {};
import initAuditEvents from 'ee/audit_events/init_audit_events';
initAuditEvents('#js-group-audit-events-app');
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module AuditEvents module AuditEvents
module AuditLogsParams module AuditLogsParams
def audit_logs_params def audit_logs_params
params.permit(:entity_type, :entity_id, :created_before, :created_after, :sort) params.permit(:entity_type, :entity_id, :created_before, :created_after, :sort, :author_id)
end end
end end
end end
...@@ -12,13 +12,26 @@ class Groups::AuditEventsController < Groups::ApplicationController ...@@ -12,13 +12,26 @@ class Groups::AuditEventsController < Groups::ApplicationController
def index def index
level = Gitlab::Audit::Levels::Group.new(group: group) level = Gitlab::Audit::Levels::Group.new(group: group)
# This is an interim change until we have proper API support within Audit Events
params = transform_author_entity_type(audit_logs_params)
events = AuditLogFinder events = AuditLogFinder
.new(level: level, params: audit_logs_params) .new(level: level, params: params)
.execute .execute
.page(params[:page]) .page(params[:page])
.without_count .without_count
@events = Gitlab::Audit::Events::Preloader.preload!(events) @events = Gitlab::Audit::Events::Preloader.preload!(events)
@table_events = AuditEventSerializer.new.represent(@events)
end
private
def transform_author_entity_type(params)
return params unless params[:entity_type] == 'Author'
params[:author_id] = params[:entity_id]
params.except(:entity_type, :entity_id)
end end
end end
...@@ -31,6 +31,7 @@ class AuditLogFinder ...@@ -31,6 +31,7 @@ class AuditLogFinder
audit_events = init_collection audit_events = init_collection
audit_events = by_entity(audit_events) audit_events = by_entity(audit_events)
audit_events = by_created_at(audit_events) audit_events = by_created_at(audit_events)
audit_events = by_author(audit_events)
sort(audit_events) sort(audit_events)
end end
...@@ -61,6 +62,12 @@ class AuditLogFinder ...@@ -61,6 +62,12 @@ class AuditLogFinder
audit_events audit_events
end end
def by_author(audit_events)
return audit_events unless valid_author_id?
audit_events.by_author_id(params[:author_id])
end
def sort(audit_events) def sort(audit_events)
audit_events.order_by(params[:sort]) audit_events.order_by(params[:sort])
end end
...@@ -72,4 +79,8 @@ class AuditLogFinder ...@@ -72,4 +79,8 @@ class AuditLogFinder
def valid_entity_id? def valid_entity_id?
params[:entity_id].to_i.nonzero? params[:entity_id].to_i.nonzero?
end end
def valid_author_id?
params[:author_id].to_i.nonzero?
end
end end
# frozen_string_literal: true # frozen_string_literal: true
module AuditEventsHelper module AuditEventsHelper
FILTER_TOKEN_TYPES = {
user: :user,
group: :group,
project: :project,
group_member: :group_member
}.freeze
def admin_audit_event_tokens
[{ type: FILTER_TOKEN_TYPES[:user] }, { type: FILTER_TOKEN_TYPES[:group] }, { type: FILTER_TOKEN_TYPES[:project] }].freeze
end
def group_audit_event_tokens(group_id)
[{ type: FILTER_TOKEN_TYPES[:group_member], group_id: group_id }]
end
def human_text(details) def human_text(details)
return details[:custom_message] if details[:custom_message] return details[:custom_message] if details[:custom_message]
......
# frozen_string_literal: true
module AuditLogsHelper
def admin_audit_log_token_types
%w[User Group Project].freeze
end
end
...@@ -5,4 +5,4 @@ ...@@ -5,4 +5,4 @@
is_last_page: @events.last_page?.to_json, is_last_page: @events.last_page?.to_json,
filter_qa_selector: 'admin_audit_log_filter', filter_qa_selector: 'admin_audit_log_filter',
table_qa_selector: 'admin_audit_log_table', table_qa_selector: 'admin_audit_log_table',
enabled_token_types: admin_audit_log_token_types } } filter_token_options: admin_audit_event_tokens.to_json } }
...@@ -3,5 +3,9 @@ ...@@ -3,5 +3,9 @@
%h3.page-title= _('Group Audit Events') %h3.page-title= _('Group Audit Events')
%p.light= _('Events in %{group_name}') % { group_name: @group.name } %p.light= _('Events in %{group_name}') % { group_name: @group.name }
= render 'shared/audit_events/event_filter', path: group_audit_events_path(@group) #js-group-audit-events-app{ data: { form_path: group_audit_events_path(@group),
= render 'shared/audit_events/event_table', events: @events events: @table_events.to_json,
is_last_page: @events.last_page?.to_json,
filter_qa_selector: 'group_audit_log_filter',
table_qa_selector: 'group_audit_log_table',
filter_token_options: group_audit_event_tokens(@group.id).to_json } }
---
title: Update group audit events to use the new searchable table
merge_request: 33305
author:
type: changed
...@@ -9,9 +9,11 @@ RSpec.describe Groups::AuditEventsController do ...@@ -9,9 +9,11 @@ RSpec.describe Groups::AuditEventsController do
describe 'GET #index' do describe 'GET #index' do
let(:sort) { nil } let(:sort) { nil }
let(:entity_type) { nil }
let(:entity_id) { nil }
let(:request) do let(:request) do
get :index, params: { group_id: group.to_param, sort: sort } get :index, params: { group_id: group.to_param, sort: sort, entity_type: entity_type, entity_id: entity_id }
end end
context 'authorized' do context 'authorized' do
...@@ -22,7 +24,7 @@ RSpec.describe Groups::AuditEventsController do ...@@ -22,7 +24,7 @@ RSpec.describe Groups::AuditEventsController do
context 'when audit_events feature is available' do context 'when audit_events feature is available' do
let(:level) { Gitlab::Audit::Levels::Group.new(group: group) } let(:level) { Gitlab::Audit::Levels::Group.new(group: group) }
let(:audit_logs_params) { ActionController::Parameters.new(sort: '').permit! } let(:audit_logs_params) { ActionController::Parameters.new(sort: '', entity_type: '', entity_id: '').permit! }
before do before do
stub_licensed_features(audit_events: true) stub_licensed_features(audit_events: true)
...@@ -31,6 +33,16 @@ RSpec.describe Groups::AuditEventsController do ...@@ -31,6 +33,16 @@ RSpec.describe Groups::AuditEventsController do
allow(AuditLogFinder).to receive(:new).and_call_original allow(AuditLogFinder).to receive(:new).and_call_original
end end
shared_examples 'AuditLogFinder params' do
it 'has the correct params' do
request
expect(AuditLogFinder).to have_received(:new).with(
level: level, params: audit_logs_params
)
end
end
it 'renders index with 200 status code' do it 'renders index with 200 status code' do
request request
...@@ -38,12 +50,22 @@ RSpec.describe Groups::AuditEventsController do ...@@ -38,12 +50,22 @@ RSpec.describe Groups::AuditEventsController do
expect(response).to render_template(:index) expect(response).to render_template(:index)
end end
it 'invokes AuditLogFinder with correct arguments' do context 'invokes AuditLogFinder with correct arguments' do
request it_behaves_like 'AuditLogFinder params'
end
context 'author' do
context 'when no author entity type is specified' do
it_behaves_like 'AuditLogFinder params'
end
expect(AuditLogFinder).to have_received(:new).with( context 'when the author entity type is specified' do
level: level, params: audit_logs_params let(:entity_type) { 'Author' }
) let(:entity_id) { 1 }
let(:audit_logs_params) { ActionController::Parameters.new(sort: '', author_id: '1').permit! }
it_behaves_like 'AuditLogFinder params'
end
end end
context 'ordering' do context 'ordering' do
......
...@@ -56,23 +56,43 @@ RSpec.describe 'Groups > Audit Events', :js do ...@@ -56,23 +56,43 @@ RSpec.describe 'Groups > Audit Events', :js do
click_link 'Audit Events' click_link 'Audit Events'
page.within('#audits') do page.within('.audit-log-table') do
expect(page).to have_content 'Change access level from developer to maintainer' expect(page).to have_content 'Changed access level from Developer to Maintainer'
expect(page).to have_content(user.name) expect(page).to have_content(user.name)
expect(page).to have_content('Alex') expect(page).to have_content('Alex')
end end
end end
end end
describe 'filter by date', js: false do describe 'filter by date' do
let!(:audit_event_1) { create(:group_audit_event, entity_type: 'Group', entity_id: group.id, created_at: 5.days.ago) } let!(:audit_event_1) { create(:group_audit_event, entity_type: 'Group', entity_id: group.id, created_at: 5.days.ago) }
let!(:audit_event_2) { create(:group_audit_event, entity_type: 'Group', entity_id: group.id, created_at: 3.days.ago) } let!(:audit_event_2) { create(:group_audit_event, entity_type: 'Group', entity_id: group.id, created_at: 3.days.ago) }
let!(:audit_event_3) { create(:group_audit_event, entity_type: 'Group', entity_id: group.id, created_at: 1.day.ago) } let!(:audit_event_3) { create(:group_audit_event, entity_type: 'Group', entity_id: group.id, created_at: 1.day.ago) }
before do it 'shows only 2 days old events' do
visit group_audit_events_path(group) visit group_audit_events_path(group, created_after: 4.days.ago.to_date, created_before: 2.days.ago.to_date)
find('.audit-log-table td', match: :first)
expect(page).not_to have_content(audit_event_1.present.date)
expect(page).to have_content(audit_event_2.present.date)
expect(page).not_to have_content(audit_event_3.present.date)
end end
it_behaves_like 'audit events filter' it 'shows only yesterday events' do
visit group_audit_events_path(group, created_after: 2.days.ago.to_date)
find('.audit-log-table td', match: :first)
expect(page).not_to have_content(audit_event_1.present.date)
expect(page).not_to have_content(audit_event_2.present.date)
expect(page).to have_content(audit_event_3.present.date)
end
it 'shows a message if provided date is invalid' do
visit group_audit_events_path(group, created_after: '12-345-6789')
expect(page).to have_content('Invalid date format. Please use UTC format as YYYY-MM-DD')
end
end end
end end
...@@ -3,15 +3,16 @@ ...@@ -3,15 +3,16 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe AuditLogFinder do RSpec.describe AuditLogFinder do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) } let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:project) { create(:project, namespace: group) } let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:subproject) { create(:project, namespace: subgroup) } let_it_be(:subproject) { create(:project, namespace: subgroup) }
let_it_be(:user_audit_event) { create(:user_audit_event, created_at: 3.days.ago) } let_it_be(:user_audit_event) { create(:user_audit_event, created_at: 3.days.ago) }
let_it_be(:project_audit_event) { create(:project_audit_event, entity_id: project.id, created_at: 2.days.ago) } let_it_be(:project_audit_event) { create(:project_audit_event, entity_id: project.id, author_id: user.id, created_at: 2.days.ago) }
let_it_be(:subproject_audit_event) { create(:project_audit_event, entity_id: subproject.id, created_at: 2.days.ago) } let_it_be(:subproject_audit_event) { create(:project_audit_event, entity_id: subproject.id, created_at: 2.days.ago) }
let_it_be(:group_audit_event) { create(:group_audit_event, entity_id: group.id, created_at: 1.day.ago) } let_it_be(:group_audit_event) { create(:group_audit_event, entity_id: group.id, author_id: user.id, created_at: 1.day.ago) }
let(:level) { Gitlab::Audit::Levels::Instance.new } let(:level) { Gitlab::Audit::Levels::Instance.new }
let(:params) { {} } let(:params) { {} }
...@@ -180,6 +181,59 @@ RSpec.describe AuditLogFinder do ...@@ -180,6 +181,59 @@ RSpec.describe AuditLogFinder do
end end
end end
context 'filtering by author_id' do
context 'no author_id provided' do
let(:params) { { entity_type: 'Author' } }
it_behaves_like 'no filtering'
end
context 'invalid author_id' do
let(:params) { { author_id: '0' } }
it 'ignores author_id and returns all events irrespective of entity_type' do
expect(subject.count).to eq(4)
end
end
shared_examples 'finds the right event' do
it 'finds the right event' do
expect(subject.count).to eq(1)
entity = subject.first
expect(entity.entity_type).to eq(entity_type)
expect(entity.id).to eq(audit_event.id)
expect(entity.author_id).to eq(audit_event.author_id)
end
end
context 'Group Event' do
let(:level) { Gitlab::Audit::Levels::Group.new(group: group) }
let(:params) { { author_id: group_audit_event.author_id } }
before do
# Only looking for group event, with this on it tests Group and Project events
stub_feature_flags(audit_log_group_level: false)
end
it_behaves_like 'finds the right event' do
let(:entity_type) { 'Group' }
let(:audit_event) { group_audit_event }
end
end
context 'Project Event' do
let(:level) { Gitlab::Audit::Levels::Project.new(project: project) }
let(:params) { { author_id: project_audit_event.author_id } }
it_behaves_like 'finds the right event' do
let(:entity_type) { 'Project' }
let(:audit_event) { project_audit_event }
end
end
end
context 'filtering by created_at' do context 'filtering by created_at' do
context 'through created_after' do context 'through created_after' do
let(:params) { { created_after: group_audit_event.created_at } } let(:params) { { created_after: group_audit_event.created_at } }
......
...@@ -17,7 +17,7 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = ` ...@@ -17,7 +17,7 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = `
data-testid="audit-events-filter" data-testid="audit-events-filter"
> >
<gl-filtered-search-stub <gl-filtered-search-stub
availabletokens="[object Object],[object Object],[object Object]" availabletokens="[object Object],[object Object],[object Object],[object Object]"
class="gl-h-32 w-100" class="gl-h-32 w-100"
clearbuttontitle="Clear" clearbuttontitle="Clear"
close-button-title="Close" close-button-title="Close"
......
...@@ -18,7 +18,7 @@ describe('AuditEventsApp', () => { ...@@ -18,7 +18,7 @@ describe('AuditEventsApp', () => {
let store; let store;
const events = [{ foo: 'bar' }]; const events = [{ foo: 'bar' }];
const enabledTokenTypes = AVAILABLE_TOKEN_TYPES; const filterTokenOptions = AVAILABLE_TOKEN_TYPES.map(type => ({ type }));
const filterQaSelector = 'filter_qa_selector'; const filterQaSelector = 'filter_qa_selector';
const tableQaSelector = 'table_qa_selector'; const tableQaSelector = 'table_qa_selector';
...@@ -29,7 +29,7 @@ describe('AuditEventsApp', () => { ...@@ -29,7 +29,7 @@ describe('AuditEventsApp', () => {
isLastPage: true, isLastPage: true,
filterQaSelector, filterQaSelector,
tableQaSelector, tableQaSelector,
enabledTokenTypes, filterTokenOptions,
events, events,
...props, ...props,
}, },
...@@ -74,7 +74,7 @@ describe('AuditEventsApp', () => { ...@@ -74,7 +74,7 @@ describe('AuditEventsApp', () => {
it('renders audit events filter', () => { it('renders audit events filter', () => {
expect(wrapper.find(AuditEventsFilter).props()).toEqual({ expect(wrapper.find(AuditEventsFilter).props()).toEqual({
enabledTokenTypes, filterTokenOptions,
qaSelector: filterQaSelector, qaSelector: filterQaSelector,
value: TEST_FILTER_VALUE, value: TEST_FILTER_VALUE,
}); });
......
...@@ -7,11 +7,10 @@ import { AVAILABLE_TOKEN_TYPES } from 'ee/audit_events/constants'; ...@@ -7,11 +7,10 @@ import { AVAILABLE_TOKEN_TYPES } from 'ee/audit_events/constants';
describe('AuditEventsFilter', () => { describe('AuditEventsFilter', () => {
let wrapper; let wrapper;
const value = [{ type: 'Project', value: { data: 1, operator: '=' } }]; const value = [{ type: 'project', value: { data: 1, operator: '=' } }];
const findFilteredSearch = () => wrapper.find(GlFilteredSearch); const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
const getAvailableTokens = () => findFilteredSearch().props('availableTokens'); const getAvailableTokens = () => findFilteredSearch().props('availableTokens');
const getAvailableTokenProps = type => const getAvailableTokenProps = type => getAvailableTokens().find(token => token.type === type);
getAvailableTokens().filter(token => token.type === type)[0];
const initComponent = (props = {}) => { const initComponent = (props = {}) => {
wrapper = shallowMount(AuditEventsFilter, { wrapper = shallowMount(AuditEventsFilter, {
...@@ -27,10 +26,11 @@ describe('AuditEventsFilter', () => { ...@@ -27,10 +26,11 @@ describe('AuditEventsFilter', () => {
}); });
describe.each` describe.each`
type | title type | title
${'Project'} | ${'Project Events'} ${'project'} | ${'Project Events'}
${'Group'} | ${'Group Events'} ${'group'} | ${'Group Events'}
${'User'} | ${'User Events'} ${'user'} | ${'User Events'}
${'group_member'} | ${'Member Events'}
`('for the list of available tokens', ({ type, title }) => { `('for the list of available tokens', ({ type, title }) => {
it(`creates a unique token for ${type}`, () => { it(`creates a unique token for ${type}`, () => {
initComponent(); initComponent();
...@@ -52,9 +52,9 @@ describe('AuditEventsFilter', () => { ...@@ -52,9 +52,9 @@ describe('AuditEventsFilter', () => {
}); });
it('only one token matching the selected token type is enabled', () => { it('only one token matching the selected token type is enabled', () => {
expect(getAvailableTokenProps('Project').disabled).toEqual(false); expect(getAvailableTokenProps('project').disabled).toEqual(false);
expect(getAvailableTokenProps('Group').disabled).toEqual(true); expect(getAvailableTokenProps('group').disabled).toEqual(true);
expect(getAvailableTokenProps('User').disabled).toEqual(true); expect(getAvailableTokenProps('user').disabled).toEqual(true);
}); });
describe('and the user submits the search field', () => { describe('and the user submits the search field', () => {
...@@ -103,11 +103,11 @@ describe('AuditEventsFilter', () => { ...@@ -103,11 +103,11 @@ describe('AuditEventsFilter', () => {
beforeEach(() => { beforeEach(() => {
initComponent({ initComponent({
enabledTokenTypes: [type], filterTokenOptions: [{ type }],
}); });
}); });
it('only the enabled token type is available for selection', () => { it('only the enabled tokens type is available for selection', () => {
expect(getAvailableTokens().length).toEqual(1); expect(getAvailableTokens().length).toEqual(1);
expect(getAvailableTokens()).toMatchObject([{ type }]); expect(getAvailableTokens()).toMatchObject([{ type }]);
}); });
......
...@@ -33,7 +33,7 @@ describe('AuditFilterToken', () => { ...@@ -33,7 +33,7 @@ describe('AuditFilterToken', () => {
propsData: { propsData: {
value: {}, value: {},
config: { config: {
type: 'Foo', type: 'foo',
}, },
active: false, active: false,
...tokenMethods, ...tokenMethods,
...@@ -108,13 +108,14 @@ describe('AuditFilterToken', () => { ...@@ -108,13 +108,14 @@ describe('AuditFilterToken', () => {
describe('when fetching suggestions', () => { describe('when fetching suggestions', () => {
let resolveSuggestions; let resolveSuggestions;
let rejectSuggestions; let rejectSuggestions;
const fetchSuggestions = () =>
new Promise((resolve, reject) => {
resolveSuggestions = resolve;
rejectSuggestions = reject;
});
beforeEach(() => { beforeEach(() => {
const value = { data: '' }; const value = { data: '' };
const fetchSuggestions = () =>
new Promise((resolve, reject) => {
resolveSuggestions = resolve;
rejectSuggestions = reject;
});
initComponent({ value, fetchSuggestions }); initComponent({ value, fetchSuggestions });
}); });
...@@ -144,6 +145,19 @@ describe('AuditFilterToken', () => { ...@@ -144,6 +145,19 @@ describe('AuditFilterToken', () => {
); );
}); });
}); });
describe('and the fetch fails with a multi-word type', () => {
beforeEach(() => {
initComponent({ config: { type: 'foo_bar' }, fetchSuggestions });
rejectSuggestions({ response: { status: httpStatusCodes.NOT_FOUND } });
});
it('shows a flash error message', () => {
expect(createFlash).toHaveBeenCalledWith(
'Failed to find foo bar. Please search for another foo bar.',
);
});
});
}); });
describe('when fetching the view item', () => { describe('when fetching the view item', () => {
......
...@@ -115,7 +115,7 @@ describe('Audit Event actions', () => { ...@@ -115,7 +115,7 @@ describe('Audit Event actions', () => {
created_after: new Date('2020-06-05T00:00:00.000Z'), created_after: new Date('2020-06-05T00:00:00.000Z'),
created_before: new Date('2020-06-25T00:00:00.000Z'), created_before: new Date('2020-06-25T00:00:00.000Z'),
entity_id: '44', entity_id: '44',
entity_type: 'User', entity_type: 'user',
sort: 'created_desc', sort: 'created_desc',
}, },
}, },
......
import { parseAuditEventSearchQuery, createAuditEventSearchQuery } from 'ee/audit_events/utils'; import {
isNumeric,
getTypeFromEntityType,
getEntityTypeFromType,
parseAuditEventSearchQuery,
createAuditEventSearchQuery,
} from 'ee/audit_events/utils';
describe('Audit Event Utils', () => { describe('Audit Event Utils', () => {
describe('isNumeric', () => {
describe.each`
value
${false}
${true}
${undefined}
${null}
${'abcd'}
${''}
`('for a list of non-numeric values', ({ value }) => {
it(`returns false for ${value}`, () => {
expect(isNumeric(value)).toBe(false);
});
});
describe.each`
value
${0}
${12345}
${'0'}
${'6789'}
`('for a list of numeric values', ({ value }) => {
it(`returns true for ${value}`, () => {
expect(isNumeric(value)).toBe(true);
});
});
});
describe('getTypeFromEntityType', () => {
it('returns the correct type when given a valid entity type', () => {
expect(getTypeFromEntityType('User')).toEqual('user');
});
it('returns `undefined` when given an invalid entity type', () => {
expect(getTypeFromEntityType('ABCDEF')).toBeUndefined();
});
});
describe('getEntityTypeFromType', () => {
it('returns the correct entity type when given a valid type', () => {
expect(getEntityTypeFromType('group_member')).toEqual('Author');
});
it('returns `undefined` when given an invalid type', () => {
expect(getTypeFromEntityType('abcdef')).toBeUndefined();
});
});
describe('parseAuditEventSearchQuery', () => { describe('parseAuditEventSearchQuery', () => {
it('returns a query object with parsed date values', () => { it('returns a query object with parsed date values', () => {
const input = { const input = {
...@@ -20,7 +74,7 @@ describe('Audit Event Utils', () => { ...@@ -20,7 +74,7 @@ describe('Audit Event Utils', () => {
describe('createAuditEventSearchQuery', () => { describe('createAuditEventSearchQuery', () => {
it('returns a query object with remapped keys and stringified dates', () => { it('returns a query object with remapped keys and stringified dates', () => {
const input = { const input = {
filterValue: [{ type: 'User', value: { data: '1', operator: '=' } }], filterValue: [{ type: 'user', value: { data: '1', operator: '=' } }],
startDate: new Date('2020-03-13'), startDate: new Date('2020-03-13'),
endDate: new Date('2020-04-13'), endDate: new Date('2020-04-13'),
sortBy: 'bar', sortBy: 'bar',
......
import { sample } from 'lodash'; import { sample } from 'lodash';
import { AVAILABLE_TOKEN_TYPES } from 'ee/audit_events/constants'; import { AVAILABLE_TOKEN_TYPES } from 'ee/audit_events/constants';
import { availableTokensValidator } from 'ee/audit_events/validators'; import { filterTokenOptionsValidator } from 'ee/audit_events/validators';
describe('availableTokensValidator', () => { describe('filterTokenOptionsValidator', () => {
it('returns true when the input contains an available token type', () => { it('returns true when the input contains a valid token type', () => {
const input = [sample(AVAILABLE_TOKEN_TYPES)]; const input = [{ type: sample(AVAILABLE_TOKEN_TYPES) }];
expect(availableTokensValidator(input)).toEqual(true); expect(filterTokenOptionsValidator(input)).toEqual(true);
}); });
it('returns false when the input contains an unavailable token type', () => {
const input = ['InvalidType']; it('returns false when the input contains an invalid token type', () => {
expect(availableTokensValidator(input)).toEqual(false); const input = [{ type: 'InvalidType' }];
expect(filterTokenOptionsValidator(input)).toEqual(false);
}); });
}); });
...@@ -3,6 +3,23 @@ ...@@ -3,6 +3,23 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe AuditEventsHelper do RSpec.describe AuditEventsHelper do
using RSpec::Parameterized::TableSyntax
describe '#admin_audit_event_tokens' do
it 'returns the available tokens' do
available_tokens = [{ type: AuditEventsHelper::FILTER_TOKEN_TYPES[:user] }, { type: AuditEventsHelper::FILTER_TOKEN_TYPES[:group] }, { type: AuditEventsHelper::FILTER_TOKEN_TYPES[:project] }]
expect(admin_audit_event_tokens).to eq(available_tokens)
end
end
describe '#group_audit_event_tokens' do
let(:group_id) { 1 }
it 'returns the available tokens' do
available_tokens = [{ type: AuditEventsHelper::FILTER_TOKEN_TYPES[:group_member], group_id: group_id }]
expect(group_audit_event_tokens(group_id)).to eq(available_tokens)
end
end
describe '#human_text' do describe '#human_text' do
let(:target_type) { 'User' } let(:target_type) { 'User' }
let(:details) do let(:details) do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AuditLogsHelper do
using RSpec::Parameterized::TableSyntax
describe '#admin_audit_log_token_types' do
it 'returns the available tokens' do
available_tokens = %w[User Group Project]
expect(admin_audit_log_token_types).to eq(available_tokens)
end
end
end
...@@ -3094,6 +3094,9 @@ msgstr "" ...@@ -3094,6 +3094,9 @@ msgstr ""
msgid "AuditLogs|IP Address" msgid "AuditLogs|IP Address"
msgstr "" msgstr ""
msgid "AuditLogs|Member Events"
msgstr ""
msgid "AuditLogs|No matching %{type} found." msgid "AuditLogs|No matching %{type} found."
msgstr "" msgstr ""
......
...@@ -56,7 +56,7 @@ module QA ...@@ -56,7 +56,7 @@ module QA
Page::Group::Menu.perform(&:click_group_general_settings_item) Page::Group::Menu.perform(&:click_group_general_settings_item)
end end
it_behaves_like 'audit event', ['Add group'] do it_behaves_like 'audit event', ['Added group'] do
let(:group) do let(:group) do
Resource::Group.fabricate_via_api! do |group| Resource::Group.fabricate_via_api! do |group|
group.name = group_name group.name = group_name
...@@ -75,7 +75,7 @@ module QA ...@@ -75,7 +75,7 @@ module QA
settings.click_save_name_visibility_settings_button settings.click_save_name_visibility_settings_button
end end
end end
it_behaves_like 'audit event', ['Change repository size limit'] it_behaves_like 'audit event', ['Changed repository size limit']
end end
context 'Update group name' do context 'Update group name' do
...@@ -90,7 +90,7 @@ module QA ...@@ -90,7 +90,7 @@ module QA
end end
end end
it_behaves_like 'audit event', ['Change name'] it_behaves_like 'audit event', ['Changed name']
end end
context 'Add user, change access level, remove user' do context 'Add user, change access level, remove user' do
...@@ -105,7 +105,7 @@ module QA ...@@ -105,7 +105,7 @@ module QA
end end
end end
it_behaves_like 'audit event', ['Add user access as guest', 'Change access level', 'Remove user access'] it_behaves_like 'audit event', ['Added user access as Guest', 'Changed access level', 'Removed user access']
end end
context 'Add and remove project access' do context 'Add and remove project access' do
...@@ -126,7 +126,7 @@ module QA ...@@ -126,7 +126,7 @@ module QA
@group.visit! @group.visit!
end end
it_behaves_like 'audit event', ['Add project access', 'Remove project access'] it_behaves_like 'audit event', ['Added project access', 'Removed project access']
end end
end end
......
...@@ -31,7 +31,7 @@ module QA ...@@ -31,7 +31,7 @@ module QA
Page::Group::Settings::General.perform(&:set_lfs_enabled) Page::Group::Settings::General.perform(&:set_lfs_enabled)
end end
it_behaves_like 'audit event', ["Change lfs enabled from false to true", "Change lfs enabled from true to false"] it_behaves_like 'audit event', ["Changed lfs enabled from false to true", "Changed lfs enabled from true to false"]
end end
context 'Enable and disable LFS' do context 'Enable and disable LFS' do
...@@ -45,7 +45,7 @@ module QA ...@@ -45,7 +45,7 @@ module QA
Page::Group::Settings::General.perform(&:set_membership_lock_disabled) Page::Group::Settings::General.perform(&:set_membership_lock_disabled)
end end
it_behaves_like 'audit event', ["Change membership lock from true to false", "Change membership lock from false to true"] it_behaves_like 'audit event', ["Changed membership lock from true to false", "Changed membership lock from false to true"]
end end
context 'Enable and disable allow user request access' do context 'Enable and disable allow user request access' do
...@@ -59,7 +59,7 @@ module QA ...@@ -59,7 +59,7 @@ module QA
Page::Group::Settings::General.perform(&:toggle_request_access) Page::Group::Settings::General.perform(&:toggle_request_access)
end end
it_behaves_like 'audit event', ["Change request access enabled from true to false", "Change request access enabled from false to true"] it_behaves_like 'audit event', ["Changed request access enabled from true to false", "Changed request access enabled from false to true"]
end end
# Bug issue: https://gitlab.com/gitlab-org/gitlab/issues/31764 # Bug issue: https://gitlab.com/gitlab-org/gitlab/issues/31764
...@@ -76,7 +76,7 @@ module QA ...@@ -76,7 +76,7 @@ module QA
Page::Group::Settings::General.perform(&:set_require_2fa_disabled) Page::Group::Settings::General.perform(&:set_require_2fa_disabled)
end end
it_behaves_like 'audit event', ["Change require two factor authentication from true to false", "Change require two factor authentication from false to true"] it_behaves_like 'audit event', ["Changed require two factor authentication from true to false", "Changed require two factor authentication from false to true"]
end end
context 'Change project creation level' do context 'Change project creation level' do
...@@ -89,7 +89,7 @@ module QA ...@@ -89,7 +89,7 @@ module QA
end end
end end
it_behaves_like 'audit event', ["Change project creation level"] it_behaves_like 'audit event', ["Changed project creation level"]
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment