Commit b11b5142 authored by Peter Hegman's avatar Peter Hegman

Merge branch '335016-filter-audit-events-by-username-frontend' into 'master'

[Frontend] Update audit events filter bar to search by username

See merge request gitlab-org/gitlab!73742
parents 10b55756 406fef79
......@@ -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