Commit 406fef79 authored by Jiaan Louw's avatar Jiaan Louw Committed by peterhegman

Update audit events to filter by username

This updates the audit events filter bar to allow the user to
filter the event entity or author by username instead of user ID.

It also adds specs for the audit filter tokens.

Changelog: changed
EE: true
parent ffbd0852
......@@ -24,3 +24,5 @@ export const DEFAULT_TH_CLASSES =
// We set the drawer's z-index to 252 to clear flash messages that might
// be displayed in the page and that have a z-index of 251.
export const DRAWER_Z_INDEX = 252;
export const MIN_USERNAME_LENGTH = 2;
<script>
import Api from '~/api';
import { isValidEntityId } from '../../token_utils';
import AuditFilterToken from './shared/audit_filter_token.vue';
export default {
......@@ -17,6 +18,16 @@ export default {
getItemName(item) {
return item.full_name;
},
getSuggestionValue({ id }) {
return id.toString();
},
isValidIdentifier(id) {
return isValidEntityId(id);
},
findActiveItem(suggestions, id) {
const parsedId = parseInt(id, 10);
return suggestions.find((g) => g.id === parsedId);
},
},
};
</script>
......
<script>
import Api from '~/api';
import { getUser } from '~/rest_api';
import { getUsers } from '~/rest_api';
import { parseUsername, displayUsername, isValidUsername } from '../../token_utils';
import AuditFilterToken from './shared/audit_filter_token.vue';
export default {
......@@ -10,18 +11,19 @@ export default {
},
inheritAttrs: false,
tokenMethods: {
fetchItem(id) {
return getUser(id).then((res) => res.data);
fetchItem(term) {
const username = parseUsername(term);
return getUsers('', { username, per_page: 1 }).then((res) => res.data[0]);
},
fetchSuggestions(term) {
const { groupId, projectPath } = this.config;
if (groupId) {
return Api.groupMembers(groupId, { search: term }).then((res) => res.data);
return Api.groupMembers(groupId, { query: parseUsername(term) }).then((res) => res.data);
}
if (projectPath) {
return Api.projectUsers(projectPath, term);
return Api.projectUsers(projectPath, parseUsername(term));
}
return {};
......@@ -29,6 +31,15 @@ export default {
getItemName({ name }) {
return name;
},
getSuggestionValue({ username }) {
return displayUsername(username);
},
isValidIdentifier(username) {
return isValidUsername(username);
},
findActiveItem(suggestions, username) {
return suggestions.find((u) => u.username === parseUsername(username));
},
},
};
</script>
......
<script>
import Api from '~/api';
import { isValidEntityId } from '../../token_utils';
import AuditFilterToken from './shared/audit_filter_token.vue';
export default {
......@@ -17,6 +18,16 @@ export default {
getItemName({ name }) {
return name;
},
getSuggestionValue({ id }) {
return id.toString();
},
isValidIdentifier(id) {
return isValidEntityId(id);
},
findActiveItem(suggestions, id) {
const parsedId = parseInt(id, 10);
return suggestions.find((p) => p.id === parsedId);
},
},
};
</script>
......
......@@ -8,7 +8,6 @@ import {
import { debounce } from 'lodash';
import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { isNumeric } from '~/lib/utils/number_utils';
import { sprintf, s__, __ } from '~/locale';
export default {
......@@ -44,6 +43,18 @@ export default {
type: Function,
required: true,
},
getSuggestionValue: {
type: Function,
required: true,
},
findActiveItem: {
type: Function,
required: true,
},
isValidIdentifier: {
type: Function,
required: true,
},
},
data() {
return {
......@@ -77,14 +88,14 @@ export default {
},
active() {
const { data: input } = this.value;
if (isNumeric(input)) {
this.selectActiveItem(parseInt(input, 10));
if (this.isValidIdentifier(input)) {
this.activeItem = this.findActiveItem(this.suggestions, input);
}
},
},
mounted() {
const { data: id } = this.value;
if (id && isNumeric(id)) {
if (this.isValidIdentifier(id)) {
this.loadView(id);
} else {
this.loadSuggestions();
......@@ -106,14 +117,14 @@ export default {
message: sprintf(message, { type }),
});
},
selectActiveItem(id) {
this.activeItem = this.suggestions.find((u) => u.id === id);
},
loadView(id) {
this.viewLoading = true;
return this.fetchItem(id)
.then((data) => {
if (data) {
this.activeItem = data;
this.suggestions.push(data);
}
})
.catch(this.onApiError)
.finally(() => {
......@@ -152,6 +163,7 @@ export default {
:alt="getAvatarString(activeItem.name)"
shape="circle"
class="gl-mr-2"
data-testid="audit-filter-item-avatar"
/>
{{ activeItemName }}
</template>
......@@ -164,7 +176,8 @@ export default {
<gl-filtered-search-suggestion
v-for="item in suggestions"
:key="item.id"
:value="item.id.toString()"
:value="getSuggestionValue(item)"
data-testid="audit-filter-suggestion"
>
<div class="d-flex">
<gl-avatar
......
<script>
import { getUsers, getUser } from '~/rest_api';
import { getUsers } from '~/rest_api';
import { parseUsername, displayUsername, isValidUsername } from '../../token_utils';
import AuditFilterToken from './shared/audit_filter_token.vue';
export default {
......@@ -8,15 +9,25 @@ export default {
},
inheritAttrs: false,
tokenMethods: {
fetchItem(id) {
return getUser(id).then((res) => res.data);
fetchItem(term) {
const username = parseUsername(term);
return getUsers('', { username, per_page: 1 }).then((res) => res.data[0]);
},
fetchSuggestions(term) {
return getUsers(term).then((res) => res.data);
return getUsers(parseUsername(term)).then((res) => res.data);
},
getItemName({ name }) {
return name;
},
getSuggestionValue({ username }) {
return displayUsername(username);
},
isValidIdentifier(username) {
return isValidUsername(username);
},
findActiveItem(suggestions, username) {
return suggestions.find((u) => u.username === parseUsername(username));
},
},
};
</script>
......
......@@ -12,7 +12,7 @@ const DEFAULT_TOKEN_OPTIONS = {
// 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 = {
export const ENTITY_TYPES = {
USER: 'User',
AUTHOR: 'Author',
GROUP: 'Group',
......
......@@ -4,14 +4,17 @@ export default {
[types.INITIALIZE_AUDIT_EVENTS](
state,
{
entity_id: id = null,
entity_id: entityId = null,
entity_username: entityUsername = null,
author_username: authorUsername = null,
entity_type: type = null,
created_after: startDate = null,
created_before: endDate = null,
sort: sortBy = null,
} = {},
) {
state.filterValue = type && id ? [{ type, value: { data: id, operator: '=' } }] : [];
const data = entityId ?? entityUsername ?? authorUsername;
state.filterValue = type && data ? [{ type, value: { data, operator: '=' } }] : [];
state.startDate = startDate;
state.endDate = endDate;
state.sortBy = sortBy;
......
// These methods need to be separate from `./utils.js` to avoid a circular dependency.
import { MIN_USERNAME_LENGTH } from '~/lib/utils/constants';
import { isNumeric } from '~/lib/utils/number_utils';
export const parseUsername = (username) =>
username && String(username).startsWith('@') ? username.slice(1) : username;
export const displayUsername = (username) => (username ? `@${username}` : null);
export const isValidUsername = (username) =>
Boolean(username) && username.length >= MIN_USERNAME_LENGTH;
export const isValidEntityId = (id) => Boolean(id) && isNumeric(id) && parseInt(id, 10) > 0;
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
import { AVAILABLE_TOKEN_TYPES, AUDIT_FILTER_CONFIGS } from './constants';
import { AVAILABLE_TOKEN_TYPES, AUDIT_FILTER_CONFIGS, ENTITY_TYPES } from './constants';
import { parseUsername, displayUsername } from './token_utils';
export const getTypeFromEntityType = (entityType) => {
return AUDIT_FILTER_CONFIGS.find(
......@@ -15,24 +16,45 @@ export const parseAuditEventSearchQuery = ({
created_after: createdAfter,
created_before: createdBefore,
entity_type: entityType,
entity_username: entityUsername,
author_username: authorUsername,
...restOfParams
}) => ({
...restOfParams,
created_after: createdAfter ? parsePikadayDate(createdAfter) : null,
created_before: createdBefore ? parsePikadayDate(createdBefore) : null,
entity_type: getTypeFromEntityType(entityType),
entity_username: displayUsername(entityUsername),
author_username: displayUsername(authorUsername),
});
export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, sortBy }) => {
const entityValue = filterValue.find((value) => AVAILABLE_TOKEN_TYPES.includes(value.type));
const entityType = getEntityTypeFromType(entityValue?.type);
const filterData = entityValue?.value.data;
return {
const params = {
created_after: startDate ? pikadayToString(startDate) : null,
created_before: endDate ? pikadayToString(endDate) : null,
sort: sortBy,
entity_id: entityValue?.value.data,
entity_type: getEntityTypeFromType(entityValue?.type),
entity_type: entityType,
entity_id: null,
entity_username: null,
author_username: null,
// When changing the search parameters, we should be resetting to the first page
page: null,
};
switch (entityType) {
case ENTITY_TYPES.USER:
params.entity_username = parseUsername(filterData);
break;
case ENTITY_TYPES.AUTHOR:
params.author_username = parseUsername(filterData);
break;
default:
params.entity_id = filterData;
}
return params;
};
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AuditFilterToken when initialized with a value matches the snapshot 1`] = `
<div
config="[object Object]"
fetchitem="function () {
return fn.apply(this, arguments);
}"
fetchsuggestions="function () {
return fn.apply(this, arguments);
}"
getitemname="function () {
return fn.apply(this, arguments);
}"
id="filtered-search-token"
value="[object Object]"
>
<div
class="view"
>
<gl-avatar-stub
alt="An item name's avatar"
class="gl-mr-2"
entityid="0"
entityname=""
shape="circle"
size="16"
src=""
/>
</div>
<div
class="suggestions"
>
<span
class="dropdown-item"
>
No matching foo found.
</span>
</div>
</div>
`;
exports[`AuditFilterToken when initialized without a value matches the snapshot 1`] = `
<div
config="[object Object]"
fetchitem="function () {
return fn.apply(this, arguments);
}"
fetchsuggestions="function () {
return fn.apply(this, arguments);
}"
getitemname="function () {
return fn.apply(this, arguments);
}"
id="filtered-search-token"
value="[object Object]"
>
<div
class="view"
/>
<div
class="suggestions"
>
<gl-filtered-search-suggestion-stub
value="1"
>
<div
class="d-flex"
>
<gl-avatar-stub
alt="A suggestion name's avatar"
entityid="1"
entityname="A suggestion name"
shape="circle"
size="32"
src="www"
/>
<div />
</div>
</gl-filtered-search-suggestion-stub>
</div>
</div>
`;
import { shallowMount } from '@vue/test-utils';
import GroupToken from 'ee/audit_events/components/tokens/group_token.vue';
import AuditFilterToken from 'ee/audit_events/components/tokens/shared/audit_filter_token.vue';
import Api from '~/api';
import { isValidEntityId } from 'ee/audit_events/token_utils';
jest.mock('~/api.js', () => ({
group: jest.fn().mockResolvedValue({ id: 1 }),
groups: jest.fn().mockResolvedValue([{ id: 1 }, { id: 2 }]),
}));
jest.mock('ee/audit_events/token_utils', () => ({
isValidEntityId: jest.fn().mockReturnValue(true),
}));
describe('GroupToken', () => {
let wrapper;
const value = { data: 123 };
const config = { type: 'foo' };
const findAuditFilterToken = () => wrapper.find(AuditFilterToken);
const initComponent = () => {
wrapper = shallowMount(GroupToken, {
propsData: {
value,
config,
active: false,
},
stubs: { AuditFilterToken },
});
};
beforeEach(() => {
initComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('binds to value, config and token methods to the filter token', () => {
expect(findAuditFilterToken().props()).toMatchObject({
value,
config,
...wrapper.vm.$options.tokenMethods,
});
});
describe('tokenMethods', () => {
it('fetchItem', async () => {
const subject = wrapper.vm.$options.tokenMethods.fetchItem;
const term = 'term';
const result = await subject(term);
expect(result).toEqual({ id: 1 });
expect(Api.group).toHaveBeenCalledWith(term);
});
it('fetchSuggestions', async () => {
const subject = wrapper.vm.$options.tokenMethods.fetchSuggestions;
const term = 'term';
const result = await subject(term);
expect(result).toEqual([{ id: 1 }, { id: 2 }]);
expect(Api.groups).toHaveBeenCalledWith(term);
});
it('getItemName', () => {
const subject = wrapper.vm.$options.tokenMethods.getItemName;
expect(subject({ full_name: 'foo' })).toBe('foo');
});
it('getSuggestionValue', () => {
const subject = wrapper.vm.$options.tokenMethods.getSuggestionValue;
const id = 123;
expect(subject({ id })).toBe('123');
});
it('isValidIdentifier', () => {
const subject = wrapper.vm.$options.tokenMethods.isValidIdentifier;
expect(subject('foo')).toBe(true);
expect(isValidEntityId).toHaveBeenCalledWith('foo');
});
it('findActiveItem', () => {
const subject = wrapper.vm.$options.tokenMethods.findActiveItem;
const suggestions = [
{ id: 1, username: 'foo' },
{ id: 2, username: 'bar' },
];
expect(subject(suggestions, 1)).toBe(suggestions[0]);
});
});
});
import { shallowMount } from '@vue/test-utils';
import MemberToken from 'ee/audit_events/components/tokens/member_token.vue';
import AuditFilterToken from 'ee/audit_events/components/tokens/shared/audit_filter_token.vue';
import Api from '~/api';
import { getUsers } from '~/rest_api';
import { displayUsername, isValidUsername } from 'ee/audit_events/token_utils';
jest.mock('~/api.js', () => ({
groupMembers: jest.fn().mockResolvedValue({ data: ['foo'] }),
projectUsers: jest.fn().mockResolvedValue(['bar']),
}));
jest.mock('~/rest_api', () => ({
getUsers: jest.fn().mockResolvedValue({
data: [{ id: 1, name: 'user' }],
}),
}));
jest.mock('ee/audit_events/token_utils', () => ({
parseUsername: jest.requireActual('ee/audit_events/token_utils').parseUsername,
displayUsername: jest.fn().mockImplementation((val) => val),
isValidUsername: jest.fn().mockReturnValue(true),
}));
describe('MemberToken', () => {
let wrapper;
const value = { data: 123 };
const config = { type: 'foo', groupId: 123 };
const findAuditFilterToken = () => wrapper.find(AuditFilterToken);
const initComponent = () => {
wrapper = shallowMount(MemberToken, {
propsData: {
value,
config,
active: false,
},
stubs: { AuditFilterToken },
});
};
beforeEach(() => {
initComponent();
});
afterEach(() => {
wrapper.destroy();
Api.groupMembers.mockClear();
Api.projectUsers.mockClear();
});
it('binds to value, config and token methods to the filter token', () => {
expect(findAuditFilterToken().props()).toMatchObject({
value,
config,
...wrapper.vm.$options.tokenMethods,
});
});
describe('tokenMethods', () => {
it('fetchItem', async () => {
const subject = wrapper.vm.$options.tokenMethods.fetchItem;
const username = 'term';
const result = await subject(username);
expect(result).toEqual({ id: 1, name: 'user' });
expect(getUsers).toHaveBeenCalledWith('', { username, per_page: 1 });
});
it('fetchSuggestions - on group level', async () => {
const context = { config: { groupId: 999 } };
const subject = wrapper.vm.$options.tokenMethods.fetchSuggestions;
const username = 'term';
const result = await subject.call(context, username);
expect(result).toEqual(['foo']);
expect(Api.groupMembers).toHaveBeenCalledWith(999, { query: username });
});
it('fetchSuggestions - on project level', async () => {
const context = { config: { projectPath: 'path' } };
const subject = wrapper.vm.$options.tokenMethods.fetchSuggestions;
const username = 'term';
const result = await subject.call(context, username);
expect(result).toEqual(['bar']);
expect(Api.projectUsers).toHaveBeenCalledWith('path', username);
});
it('getItemName', () => {
const subject = wrapper.vm.$options.tokenMethods.getItemName;
const name = 'foo';
expect(subject({ name })).toBe(name);
});
it('getSuggestionValue', () => {
const subject = wrapper.vm.$options.tokenMethods.getSuggestionValue;
const username = 'foo';
expect(subject({ username })).toBe(username);
expect(displayUsername).toHaveBeenCalledWith(username);
});
it('isValidIdentifier', () => {
const subject = wrapper.vm.$options.tokenMethods.isValidIdentifier;
expect(subject('foo')).toBe(true);
expect(isValidUsername).toHaveBeenCalledWith('foo');
});
it('findActiveItem', () => {
const subject = wrapper.vm.$options.tokenMethods.findActiveItem;
const suggestions = [
{ id: 1, username: 'foo' },
{ id: 2, username: 'bar' },
];
expect(subject(suggestions, 'foo')).toBe(suggestions[0]);
});
});
});
import { shallowMount } from '@vue/test-utils';
import ProjectToken from 'ee/audit_events/components/tokens/project_token.vue';
import AuditFilterToken from 'ee/audit_events/components/tokens/shared/audit_filter_token.vue';
import Api from '~/api';
import { isValidEntityId } from 'ee/audit_events/token_utils';
jest.mock('~/api.js', () => ({
project: jest.fn().mockResolvedValue({ data: { id: 1 } }),
projects: jest.fn().mockResolvedValue({ data: [{ id: 1 }, { id: 2 }] }),
}));
jest.mock('ee/audit_events/token_utils', () => ({
isValidEntityId: jest.fn().mockReturnValue(true),
}));
describe('ProjectToken', () => {
let wrapper;
const value = { data: 123 };
const config = { type: 'foo' };
const findAuditFilterToken = () => wrapper.find(AuditFilterToken);
const initComponent = () => {
wrapper = shallowMount(ProjectToken, {
propsData: {
value,
config,
active: false,
},
stubs: { AuditFilterToken },
});
};
beforeEach(() => {
initComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('binds to value, config and token methods to the filter token', () => {
expect(findAuditFilterToken().props()).toMatchObject({
value,
config,
...wrapper.vm.$options.tokenMethods,
});
});
describe('tokenMethods', () => {
it('fetchItem', async () => {
const subject = wrapper.vm.$options.tokenMethods.fetchItem;
const id = 123;
const result = await subject(id);
expect(result).toEqual({ id: 1 });
expect(Api.project).toHaveBeenCalledWith(id);
});
it('fetchSuggestions', async () => {
const subject = wrapper.vm.$options.tokenMethods.fetchSuggestions;
const term = 'term';
const result = await subject(term);
expect(result).toEqual([{ id: 1 }, { id: 2 }]);
expect(Api.projects).toHaveBeenCalledWith(term, { membership: false });
});
it('getItemName', () => {
const subject = wrapper.vm.$options.tokenMethods.getItemName;
expect(subject({ name: 'foo' })).toBe('foo');
});
it('getSuggestionValue', () => {
const subject = wrapper.vm.$options.tokenMethods.getSuggestionValue;
const id = 123;
expect(subject({ id })).toBe('123');
});
it('isValidIdentifier', () => {
const subject = wrapper.vm.$options.tokenMethods.isValidIdentifier;
expect(subject('foo')).toBe(true);
expect(isValidEntityId).toHaveBeenCalledWith('foo');
});
it('findActiveItem', () => {
const subject = wrapper.vm.$options.tokenMethods.findActiveItem;
const suggestions = [
{ id: 1, username: 'foo' },
{ id: 2, username: 'bar' },
];
expect(subject(suggestions, 1)).toBe(suggestions[0]);
});
});
});
import { shallowMount } from '@vue/test-utils';
import UserToken from 'ee/audit_events/components/tokens/user_token.vue';
import AuditFilterToken from 'ee/audit_events/components/tokens/shared/audit_filter_token.vue';
import { getUsers } from '~/rest_api';
import { displayUsername, isValidUsername } from 'ee/audit_events/token_utils';
jest.mock('~/rest_api', () => ({
getUsers: jest.fn().mockResolvedValue({
data: [{ id: 1, name: 'user' }],
}),
}));
jest.mock('ee/audit_events/token_utils', () => ({
parseUsername: jest.requireActual('ee/audit_events/token_utils').parseUsername,
displayUsername: jest.fn().mockImplementation((val) => val),
isValidUsername: jest.fn().mockReturnValue(true),
}));
describe('UserToken', () => {
let wrapper;
const value = { data: 123 };
const config = { type: 'foo' };
const findAuditFilterToken = () => wrapper.find(AuditFilterToken);
const initComponent = () => {
wrapper = shallowMount(UserToken, {
propsData: {
value,
config,
active: false,
},
stubs: { AuditFilterToken },
});
};
beforeEach(() => {
initComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('binds to value, config and token methods to the filter token', () => {
expect(findAuditFilterToken().props()).toMatchObject({
value,
config,
...wrapper.vm.$options.tokenMethods,
});
});
describe('tokenMethods', () => {
it('fetchItem', async () => {
const subject = wrapper.vm.$options.tokenMethods.fetchItem;
const username = 'term';
const result = await subject(username);
expect(result).toEqual({ id: 1, name: 'user' });
expect(getUsers).toHaveBeenCalledWith('', { username, per_page: 1 });
});
it('fetchSuggestions', async () => {
const subject = wrapper.vm.$options.tokenMethods.fetchSuggestions;
const username = 'term';
const result = await subject(username);
expect(result).toEqual([{ id: 1, name: 'user' }]);
expect(getUsers).toHaveBeenCalledWith(username);
});
it('getItemName', () => {
const subject = wrapper.vm.$options.tokenMethods.getItemName;
const name = 'foo';
expect(subject({ name })).toBe(name);
});
it('getSuggestionValue', () => {
const subject = wrapper.vm.$options.tokenMethods.getSuggestionValue;
const username = 'foo';
expect(subject({ username })).toBe(username);
expect(displayUsername).toHaveBeenCalledWith(username);
});
it('isValidIdentifier', () => {
const subject = wrapper.vm.$options.tokenMethods.isValidIdentifier;
expect(subject('foo')).toBe(true);
expect(isValidUsername).toHaveBeenCalledWith('foo');
});
it('findActiveItem', () => {
const subject = wrapper.vm.$options.tokenMethods.findActiveItem;
const suggestions = [
{ id: 1, username: 'foo' },
{ id: 2, username: 'bar' },
];
expect(subject(suggestions, 'foo')).toBe(suggestions[0]);
});
});
});
......@@ -41,7 +41,7 @@ describe('Audit Event actions', () => {
);
it('setFilterValue action should commit to the store', () => {
const payload = [{ type: 'User', value: { data: 1, operator: '=' } }];
const payload = [{ type: 'User', value: { data: '@root', operator: '=' } }];
testAction(actions.setFilterValue, payload, state, [{ type: types.SET_FILTER_VALUE, payload }]);
});
......@@ -91,6 +91,9 @@ describe('Audit Event actions', () => {
payload: {
created_after: null,
created_before: null,
author_username: null,
entity_username: null,
entity_type: undefined,
},
},
]);
......@@ -100,7 +103,7 @@ describe('Audit Event actions', () => {
describe('with a full search query', () => {
beforeEach(() => {
setWindowLocation(
'?sort=created_desc&entity_type=User&entity_id=44&created_after=2020-06-05&created_before=2020-06-25',
'?sort=created_desc&entity_type=Project&entity_id=44&created_after=2020-06-05&created_before=2020-06-25',
);
});
......@@ -112,8 +115,10 @@ 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: 'project',
sort: 'created_desc',
author_username: null,
entity_username: null,
},
},
]);
......
......@@ -17,7 +17,7 @@ describe('Audit Events getters', () => {
describe('with filters and dates', () => {
it('returns the export url', () => {
const filterValue = [{ type: 'user', value: { data: 1, operator: '=' } }];
const filterValue = [{ type: 'user', value: { data: '@root', operator: '=' } }];
const startDate = new Date(2020, 1, 2);
const endDate = new Date(2020, 1, 30);
const state = { ...createState, ...{ filterValue, startDate, endDate } };
......@@ -25,7 +25,7 @@ describe('Audit Events getters', () => {
expect(getters.buildExportHref(state)(exportUrl)).toEqual(
'https://example.com/audit_reports.csv?' +
'created_after=2020-02-02&created_before=2020-03-01' +
'&entity_id=1&entity_type=User',
'&entity_type=User&entity_username=root',
);
});
});
......
......@@ -38,9 +38,13 @@ describe('Audit Event mutations', () => {
sort: 'created_asc',
};
const createFilterValue = (data) => {
return [{ type: payload.entity_type, value: { data, operator: '=' } }];
};
it.each`
stateKey | expectedState
${'filterValue'} | ${[{ type: payload.entity_type, value: { data: payload.entity_id, operator: '=' } }]}
${'filterValue'} | ${createFilterValue(payload.entity_id)}
${'startDate'} | ${payload.created_after}
${'endDate'} | ${payload.created_before}
${'sortBy'} | ${payload.sort}
......@@ -50,5 +54,21 @@ describe('Audit Event mutations', () => {
expect(state[stateKey]).toEqual(expectedState);
});
it.each`
payloadKey | payloadValue
${'entity_id'} | ${'1'}
${'entity_username'} | ${'abc'}
${'author_username'} | ${'abc'}
`('sets the filter value when provided with a $payloadKey', ({ payloadKey, payloadValue }) => {
const payloadWithValue = {
...payload,
entity_id: undefined,
[payloadKey]: payloadValue,
};
mutations[types.INITIALIZE_AUDIT_EVENTS](state, payloadWithValue);
expect(state.filterValue).toEqual(createFilterValue(payloadValue));
});
});
});
import { MIN_USERNAME_LENGTH } from '~/lib/utils/constants';
import {
parseUsername,
displayUsername,
isValidUsername,
isValidEntityId,
} from 'ee/audit_events/token_utils';
describe('Audit Event Text Utils', () => {
describe('parseUsername', () => {
it('returns the username without the @ character', () => {
expect(parseUsername('@username')).toBe('username');
});
it('returns the username unchanged when it does not include a @ character', () => {
expect(parseUsername('username')).toBe('username');
});
});
describe('displayUsername', () => {
it('returns the username with the @ character', () => {
expect(displayUsername('username')).toBe('@username');
});
});
describe('isValidUsername', () => {
it('returns true if the username is valid', () => {
const username = 'a'.repeat(MIN_USERNAME_LENGTH);
expect(isValidUsername(username)).toBe(true);
});
it('returns false if the username is too short', () => {
const username = 'a'.repeat(MIN_USERNAME_LENGTH - 1);
expect(isValidUsername(username)).toBe(false);
});
it('returns false if the username is empty', () => {
const username = '';
expect(isValidUsername(username)).toBe(false);
});
});
describe('isValidEntityId', () => {
it('returns true if the entity id is a positive number', () => {
const id = 1;
expect(isValidEntityId(id)).toBe(true);
});
it('returns true if the entity id is a numeric string', () => {
const id = '123';
expect(isValidEntityId(id)).toBe(true);
});
it('returns false if the entity id is zero', () => {
const id = 0;
expect(isValidEntityId(id)).toBe(false);
});
it('returns false if the entity id is not numeric', () => {
const id = 'abc';
expect(isValidEntityId(id)).toBe(false);
});
});
});
......@@ -34,7 +34,7 @@ describe('Audit Event Utils', () => {
sortBy: 'created_asc',
};
expect(parseAuditEventSearchQuery(input)).toEqual({
expect(parseAuditEventSearchQuery(input)).toMatchObject({
created_after: new Date('2020-03-13'),
created_before: new Date('2020-04-13'),
sortBy: 'created_asc',
......@@ -43,22 +43,35 @@ 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: '=' } }],
const createFilterParams = (type, data) => ({
filterValue: [{ type, value: { data, operator: '=' } }],
startDate: new Date('2020-03-13'),
endDate: new Date('2020-04-13'),
sortBy: 'bar',
};
});
it.each`
type | entity_type | data | entity_id | entity_username | author_username
${'user'} | ${'User'} | ${'@root'} | ${null} | ${'root'} | ${null}
${'member'} | ${'Author'} | ${'@root'} | ${null} | ${null} | ${'root'}
${'project'} | ${'Project'} | ${'1'} | ${'1'} | ${null} | ${null}
${'group'} | ${'Group'} | ${'1'} | ${'1'} | ${null} | ${null}
`(
'returns a query object with remapped keys and stringified dates for type $type',
({ type, entity_type, data, entity_id, entity_username, author_username }) => {
const input = createFilterParams(type, data);
expect(createAuditEventSearchQuery(input)).toEqual({
entity_id: '1',
entity_type: 'User',
entity_id,
entity_username,
author_username,
entity_type,
created_after: '2020-03-13',
created_before: '2020-04-13',
sort: 'bar',
page: null,
});
});
},
);
});
});
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