Commit fed9026b authored by Illya Klymov's avatar Illya Klymov

Merge branch...

Merge branch '217781-refactor-the-group-audit-events-to-use-the-audit-events-shared-template' into 'master'

Refactor group audit events to use the audit events shared template

See merge request gitlab-org/gitlab!33305
parents 22e6de6e 0d4d35bb
......@@ -16,6 +16,7 @@ class AuditEvent < ApplicationRecord
scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) }
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
......
......@@ -23,7 +23,7 @@ export default {
required: false,
default: false,
},
enabledTokenTypes: {
filterTokenOptions: {
type: Array,
required: true,
},
......@@ -53,7 +53,7 @@ export default {
<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">
<audit-events-filter
:enabled-token-types="enabledTokenTypes"
:filter-token-options="filterTokenOptions"
:qa-selector="filterQaSelector"
:value="filterValue"
@selected="setFilterValue"
......
<script>
import { GlFilteredSearch } from '@gitlab/ui';
import { FILTER_TOKENS, AVAILABLE_TOKEN_TYPES } from '../constants';
import { availableTokensValidator } from '../validators';
import { AUDIT_FILTER_CONFIGS } from '../constants';
import { filterTokenOptionsValidator } from '../validators';
export default {
components: {
......@@ -13,11 +13,11 @@ export default {
required: false,
default: () => [],
},
enabledTokenTypes: {
filterTokenOptions: {
type: Array,
required: false,
default: () => AVAILABLE_TOKEN_TYPES,
validator: availableTokensValidator,
default: () => AUDIT_FILTER_CONFIGS,
validator: filterTokenOptionsValidator,
},
qaSelector: {
type: String,
......@@ -25,25 +25,30 @@ export default {
default: undefined,
},
},
data() {
return {
filterTokens: this.filterTokenOptions.map(option => ({
...AUDIT_FILTER_CONFIGS.find(({ type }) => type === option.type),
...option,
})),
};
},
computed: {
searchTerm() {
return this.value.find(term => AVAILABLE_TOKEN_TYPES.includes(term.type));
tokenSearchTerm() {
return this.value.find(term => this.filterTokens.find(token => token.type === term.type));
},
enabledTokens() {
return FILTER_TOKENS.filter(token => this.enabledTokenTypes.includes(token.type));
},
filterTokens() {
// This limits the user to search by only one of the available tokens
const { enabledTokens, searchTerm } = this;
const { tokenSearchTerm } = this;
if (searchTerm?.type) {
return enabledTokens.map(token => ({
// If a user has searched for a term within a token, limit the user to that one token
if (tokenSearchTerm) {
return this.filterTokens.map(token => ({
...token,
disabled: searchTerm.type !== token.type,
disabled: tokenSearchTerm.type !== token.type,
}));
}
return enabledTokens;
return this.filterTokens;
},
},
methods: {
......@@ -68,7 +73,7 @@ export default {
:placeholder="__('Search')"
:clear-button-title="__('Clear')"
:close-button-title="__('Close')"
:available-tokens="filterTokens"
:available-tokens="enabledTokens"
class="gl-h-32 w-100"
@submit="onSubmit"
@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 {
return this.suggestions.length > 0;
},
lowerCaseType() {
return this.config.type.toLowerCase();
return this.config.type
.replace('_', ' ')
.trim()
.toLowerCase();
},
noSuggestionsString() {
return sprintf(s__('AuditLogs|No matching %{type} found.'), { type: this.lowerCaseType });
......
import { __, s__ } from '~/locale';
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 GroupToken from './components/tokens/group_token.vue';
......@@ -8,28 +10,49 @@ const DEFAULT_TOKEN_OPTIONS = {
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,
icon: 'user',
title: s__('AuditLogs|User Events'),
type: 'User',
type: 'user',
entityType: ENTITY_TYPES.USER,
token: UserToken,
},
{
...DEFAULT_TOKEN_OPTIONS,
icon: 'user',
title: s__('AuditLogs|Member Events'),
type: 'group_member',
entityType: ENTITY_TYPES.AUTHOR,
token: GroupMemberToken,
},
{
...DEFAULT_TOKEN_OPTIONS,
icon: 'bookmark',
title: s__('AuditLogs|Project Events'),
type: 'Project',
type: 'project',
entityType: ENTITY_TYPES.PROJECT,
token: ProjectToken,
},
{
...DEFAULT_TOKEN_OPTIONS,
icon: 'group',
title: s__('AuditLogs|Group Events'),
type: 'Group',
type: 'group',
entityType: ENTITY_TYPES.GROUP,
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 { parseBoolean } from '~/lib/utils/common_utils';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import AuditEventsApp from './components/audit_events_app.vue';
import createStore from './store';
export default 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();
store.dispatch('initializeAuditEvents');
......@@ -18,7 +20,9 @@ export default selector => {
props: {
events: JSON.parse(events),
isLastPage: parseBoolean(isLastPage),
enabledTokenTypes: JSON.parse(enabledTokenTypes),
filterTokenOptions: JSON.parse(filterTokenOptions).map(filterTokenOption =>
convertObjectPropsToCamelCase(filterTokenOption),
),
filterQaSelector,
tableQaSelector,
},
......
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 => {
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 = ({
created_after: createdAfter,
created_before: createdBefore,
entity_type: entityType,
...restOfParams
}) => ({
...restOfParams,
created_after: createdAfter ? parsePikadayDate(createdAfter) : null,
created_before: createdBefore ? parsePikadayDate(createdBefore) : null,
entity_type: getTypeFromEntityType(entityType),
});
export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, sortBy }) => {
......@@ -23,7 +35,7 @@ export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, s
created_before: endDate ? pikadayToString(endDate) : null,
sort: sortBy,
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
page: null,
};
......
import { AVAILABLE_TOKEN_TYPES } from './constants';
export function availableTokensValidator(value) {
return value.every(type => AVAILABLE_TOKEN_TYPES.includes(type));
export function filterTokenOptionsValidator(filterTokenOptions) {
return filterTokenOptions
.map(({ type }) => type)
.every(type => AVAILABLE_TOKEN_TYPES.includes(type));
}
export default {};
import initAuditEvents from 'ee/audit_events/init_audit_events';
initAuditEvents('#js-group-audit-events-app');
......@@ -3,7 +3,7 @@
module AuditEvents
module AuditLogsParams
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
......@@ -12,13 +12,26 @@ class Groups::AuditEventsController < Groups::ApplicationController
def index
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
.new(level: level, params: audit_logs_params)
.new(level: level, params: params)
.execute
.page(params[:page])
.without_count
@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
......@@ -31,6 +31,7 @@ class AuditLogFinder
audit_events = init_collection
audit_events = by_entity(audit_events)
audit_events = by_created_at(audit_events)
audit_events = by_author(audit_events)
sort(audit_events)
end
......@@ -61,6 +62,12 @@ class AuditLogFinder
audit_events
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)
audit_events.order_by(params[:sort])
end
......@@ -72,4 +79,8 @@ class AuditLogFinder
def valid_entity_id?
params[:entity_id].to_i.nonzero?
end
def valid_author_id?
params[:author_id].to_i.nonzero?
end
end
# frozen_string_literal: true
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)
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 @@
is_last_page: @events.last_page?.to_json,
filter_qa_selector: 'admin_audit_log_filter',
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 @@
%h3.page-title= _('Group Audit Events')
%p.light= _('Events in %{group_name}') % { group_name: @group.name }
= render 'shared/audit_events/event_filter', path: group_audit_events_path(@group)
= render 'shared/audit_events/event_table', events: @events
#js-group-audit-events-app{ data: { form_path: group_audit_events_path(@group),
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
describe 'GET #index' do
let(:sort) { nil }
let(:entity_type) { nil }
let(:entity_id) { nil }
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
context 'authorized' do
......@@ -22,7 +24,7 @@ RSpec.describe Groups::AuditEventsController do
context 'when audit_events feature is available' do
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
stub_licensed_features(audit_events: true)
......@@ -31,6 +33,16 @@ RSpec.describe Groups::AuditEventsController do
allow(AuditLogFinder).to receive(:new).and_call_original
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
request
......@@ -38,12 +50,22 @@ RSpec.describe Groups::AuditEventsController do
expect(response).to render_template(:index)
end
it 'invokes AuditLogFinder with correct arguments' do
request
context 'invokes AuditLogFinder with correct arguments' do
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(
level: level, params: audit_logs_params
)
context 'when the author entity type is specified' do
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
context 'ordering' do
......
......@@ -56,23 +56,43 @@ RSpec.describe 'Groups > Audit Events', :js do
click_link 'Audit Events'
page.within('#audits') do
expect(page).to have_content 'Change access level from developer to maintainer'
page.within('.audit-log-table') do
expect(page).to have_content 'Changed access level from Developer to Maintainer'
expect(page).to have_content(user.name)
expect(page).to have_content('Alex')
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_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) }
before do
visit group_audit_events_path(group)
it 'shows only 2 days old events' do
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
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
......@@ -3,15 +3,16 @@
require 'spec_helper'
RSpec.describe AuditLogFinder do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:project) { create(:project, namespace: group) }
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(: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(: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(:params) { {} }
......@@ -180,6 +181,59 @@ RSpec.describe AuditLogFinder do
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 'through created_after' do
let(:params) { { created_after: group_audit_event.created_at } }
......
......@@ -17,7 +17,7 @@ exports[`AuditEventsApp when initialized matches the snapshot 1`] = `
data-testid="audit-events-filter"
>
<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"
clearbuttontitle="Clear"
close-button-title="Close"
......
......@@ -18,7 +18,7 @@ describe('AuditEventsApp', () => {
let store;
const events = [{ foo: 'bar' }];
const enabledTokenTypes = AVAILABLE_TOKEN_TYPES;
const filterTokenOptions = AVAILABLE_TOKEN_TYPES.map(type => ({ type }));
const filterQaSelector = 'filter_qa_selector';
const tableQaSelector = 'table_qa_selector';
......@@ -29,7 +29,7 @@ describe('AuditEventsApp', () => {
isLastPage: true,
filterQaSelector,
tableQaSelector,
enabledTokenTypes,
filterTokenOptions,
events,
...props,
},
......@@ -74,7 +74,7 @@ describe('AuditEventsApp', () => {
it('renders audit events filter', () => {
expect(wrapper.find(AuditEventsFilter).props()).toEqual({
enabledTokenTypes,
filterTokenOptions,
qaSelector: filterQaSelector,
value: TEST_FILTER_VALUE,
});
......
......@@ -7,11 +7,10 @@ import { AVAILABLE_TOKEN_TYPES } from 'ee/audit_events/constants';
describe('AuditEventsFilter', () => {
let wrapper;
const value = [{ type: 'Project', value: { data: 1, operator: '=' } }];
const value = [{ type: 'project', value: { data: 1, operator: '=' } }];
const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
const getAvailableTokens = () => findFilteredSearch().props('availableTokens');
const getAvailableTokenProps = type =>
getAvailableTokens().filter(token => token.type === type)[0];
const getAvailableTokenProps = type => getAvailableTokens().find(token => token.type === type);
const initComponent = (props = {}) => {
wrapper = shallowMount(AuditEventsFilter, {
......@@ -27,10 +26,11 @@ describe('AuditEventsFilter', () => {
});
describe.each`
type | title
${'Project'} | ${'Project Events'}
${'Group'} | ${'Group Events'}
${'User'} | ${'User Events'}
type | title
${'project'} | ${'Project Events'}
${'group'} | ${'Group Events'}
${'user'} | ${'User Events'}
${'group_member'} | ${'Member Events'}
`('for the list of available tokens', ({ type, title }) => {
it(`creates a unique token for ${type}`, () => {
initComponent();
......@@ -52,9 +52,9 @@ describe('AuditEventsFilter', () => {
});
it('only one token matching the selected token type is enabled', () => {
expect(getAvailableTokenProps('Project').disabled).toEqual(false);
expect(getAvailableTokenProps('Group').disabled).toEqual(true);
expect(getAvailableTokenProps('User').disabled).toEqual(true);
expect(getAvailableTokenProps('project').disabled).toEqual(false);
expect(getAvailableTokenProps('group').disabled).toEqual(true);
expect(getAvailableTokenProps('user').disabled).toEqual(true);
});
describe('and the user submits the search field', () => {
......@@ -103,11 +103,11 @@ describe('AuditEventsFilter', () => {
beforeEach(() => {
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()).toMatchObject([{ type }]);
});
......
......@@ -33,7 +33,7 @@ describe('AuditFilterToken', () => {
propsData: {
value: {},
config: {
type: 'Foo',
type: 'foo',
},
active: false,
...tokenMethods,
......@@ -108,13 +108,14 @@ describe('AuditFilterToken', () => {
describe('when fetching suggestions', () => {
let resolveSuggestions;
let rejectSuggestions;
const fetchSuggestions = () =>
new Promise((resolve, reject) => {
resolveSuggestions = resolve;
rejectSuggestions = reject;
});
beforeEach(() => {
const value = { data: '' };
const fetchSuggestions = () =>
new Promise((resolve, reject) => {
resolveSuggestions = resolve;
rejectSuggestions = reject;
});
initComponent({ value, fetchSuggestions });
});
......@@ -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', () => {
......
......@@ -115,7 +115,7 @@ describe('Audit Event actions', () => {
created_after: new Date('2020-06-05T00:00:00.000Z'),
created_before: new Date('2020-06-25T00:00:00.000Z'),
entity_id: '44',
entity_type: 'User',
entity_type: 'user',
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('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', () => {
it('returns a query object with parsed date values', () => {
const input = {
......@@ -20,7 +74,7 @@ describe('Audit Event Utils', () => {
describe('createAuditEventSearchQuery', () => {
it('returns a query object with remapped keys and stringified dates', () => {
const input = {
filterValue: [{ type: 'User', value: { data: '1', operator: '=' } }],
filterValue: [{ type: 'user', value: { data: '1', operator: '=' } }],
startDate: new Date('2020-03-13'),
endDate: new Date('2020-04-13'),
sortBy: 'bar',
......
import { sample } from 'lodash';
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', () => {
it('returns true when the input contains an available token type', () => {
const input = [sample(AVAILABLE_TOKEN_TYPES)];
expect(availableTokensValidator(input)).toEqual(true);
describe('filterTokenOptionsValidator', () => {
it('returns true when the input contains a valid token type', () => {
const input = [{ type: sample(AVAILABLE_TOKEN_TYPES) }];
expect(filterTokenOptionsValidator(input)).toEqual(true);
});
it('returns false when the input contains an unavailable token type', () => {
const input = ['InvalidType'];
expect(availableTokensValidator(input)).toEqual(false);
it('returns false when the input contains an invalid token type', () => {
const input = [{ type: 'InvalidType' }];
expect(filterTokenOptionsValidator(input)).toEqual(false);
});
});
......@@ -3,6 +3,23 @@
require 'spec_helper'
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
let(:target_type) { 'User' }
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
......@@ -3106,6 +3106,9 @@ msgstr ""
msgid "AuditLogs|IP Address"
msgstr ""
msgid "AuditLogs|Member Events"
msgstr ""
msgid "AuditLogs|No matching %{type} found."
msgstr ""
......
......@@ -56,7 +56,7 @@ module QA
Page::Group::Menu.perform(&:click_group_general_settings_item)
end
it_behaves_like 'audit event', ['Add group'] do
it_behaves_like 'audit event', ['Added group'] do
let(:group) do
Resource::Group.fabricate_via_api! do |group|
group.name = group_name
......@@ -75,7 +75,7 @@ module QA
settings.click_save_name_visibility_settings_button
end
end
it_behaves_like 'audit event', ['Change repository size limit']
it_behaves_like 'audit event', ['Changed repository size limit']
end
context 'Update group name' do
......@@ -90,7 +90,7 @@ module QA
end
end
it_behaves_like 'audit event', ['Change name']
it_behaves_like 'audit event', ['Changed name']
end
context 'Add user, change access level, remove user' do
......@@ -105,7 +105,7 @@ module QA
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
context 'Add and remove project access' do
......@@ -126,7 +126,7 @@ module QA
@group.visit!
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
......
......@@ -31,7 +31,7 @@ module QA
Page::Group::Settings::General.perform(&:set_lfs_enabled)
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
context 'Enable and disable LFS' do
......@@ -45,7 +45,7 @@ module QA
Page::Group::Settings::General.perform(&:set_membership_lock_disabled)
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
context 'Enable and disable allow user request access' do
......@@ -59,7 +59,7 @@ module QA
Page::Group::Settings::General.perform(&:toggle_request_access)
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
# Bug issue: https://gitlab.com/gitlab-org/gitlab/issues/31764
......@@ -76,7 +76,7 @@ module QA
Page::Group::Settings::General.perform(&:set_require_2fa_disabled)
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
context 'Change project creation level' do
......@@ -89,7 +89,7 @@ module QA
end
end
it_behaves_like 'audit event', ["Change project creation level"]
it_behaves_like 'audit event', ["Changed project creation level"]
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