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
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
......@@ -3094,6 +3094,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