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 = ...@@ -24,3 +24,5 @@ export const DEFAULT_TH_CLASSES =
// We set the drawer's z-index to 252 to clear flash messages that might // 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. // be displayed in the page and that have a z-index of 251.
export const DRAWER_Z_INDEX = 252; export const DRAWER_Z_INDEX = 252;
export const MIN_USERNAME_LENGTH = 2;
<script> <script>
import Api from '~/api'; import Api from '~/api';
import { isValidEntityId } from '../../token_utils';
import AuditFilterToken from './shared/audit_filter_token.vue'; import AuditFilterToken from './shared/audit_filter_token.vue';
export default { export default {
...@@ -17,6 +18,16 @@ export default { ...@@ -17,6 +18,16 @@ export default {
getItemName(item) { getItemName(item) {
return item.full_name; 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>
......
<script> <script>
import Api from '~/api'; 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'; import AuditFilterToken from './shared/audit_filter_token.vue';
export default { export default {
...@@ -10,18 +11,19 @@ export default { ...@@ -10,18 +11,19 @@ export default {
}, },
inheritAttrs: false, inheritAttrs: false,
tokenMethods: { tokenMethods: {
fetchItem(id) { fetchItem(term) {
return getUser(id).then((res) => res.data); const username = parseUsername(term);
return getUsers('', { username, per_page: 1 }).then((res) => res.data[0]);
}, },
fetchSuggestions(term) { fetchSuggestions(term) {
const { groupId, projectPath } = this.config; const { groupId, projectPath } = this.config;
if (groupId) { 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) { if (projectPath) {
return Api.projectUsers(projectPath, term); return Api.projectUsers(projectPath, parseUsername(term));
} }
return {}; return {};
...@@ -29,6 +31,15 @@ export default { ...@@ -29,6 +31,15 @@ export default {
getItemName({ name }) { getItemName({ name }) {
return 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>
......
<script> <script>
import Api from '~/api'; import Api from '~/api';
import { isValidEntityId } from '../../token_utils';
import AuditFilterToken from './shared/audit_filter_token.vue'; import AuditFilterToken from './shared/audit_filter_token.vue';
export default { export default {
...@@ -17,6 +18,16 @@ export default { ...@@ -17,6 +18,16 @@ export default {
getItemName({ name }) { getItemName({ name }) {
return 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> </script>
......
...@@ -8,7 +8,6 @@ import { ...@@ -8,7 +8,6 @@ import {
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { isNumeric } from '~/lib/utils/number_utils';
import { sprintf, s__, __ } from '~/locale'; import { sprintf, s__, __ } from '~/locale';
export default { export default {
...@@ -44,6 +43,18 @@ export default { ...@@ -44,6 +43,18 @@ export default {
type: Function, type: Function,
required: true, required: true,
}, },
getSuggestionValue: {
type: Function,
required: true,
},
findActiveItem: {
type: Function,
required: true,
},
isValidIdentifier: {
type: Function,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -77,14 +88,14 @@ export default { ...@@ -77,14 +88,14 @@ export default {
}, },
active() { active() {
const { data: input } = this.value; const { data: input } = this.value;
if (isNumeric(input)) { if (this.isValidIdentifier(input)) {
this.selectActiveItem(parseInt(input, 10)); this.activeItem = this.findActiveItem(this.suggestions, input);
} }
}, },
}, },
mounted() { mounted() {
const { data: id } = this.value; const { data: id } = this.value;
if (id && isNumeric(id)) { if (this.isValidIdentifier(id)) {
this.loadView(id); this.loadView(id);
} else { } else {
this.loadSuggestions(); this.loadSuggestions();
...@@ -106,14 +117,14 @@ export default { ...@@ -106,14 +117,14 @@ export default {
message: sprintf(message, { type }), message: sprintf(message, { type }),
}); });
}, },
selectActiveItem(id) {
this.activeItem = this.suggestions.find((u) => u.id === id);
},
loadView(id) { loadView(id) {
this.viewLoading = true; this.viewLoading = true;
return this.fetchItem(id) return this.fetchItem(id)
.then((data) => { .then((data) => {
if (data) {
this.activeItem = data; this.activeItem = data;
this.suggestions.push(data);
}
}) })
.catch(this.onApiError) .catch(this.onApiError)
.finally(() => { .finally(() => {
...@@ -152,6 +163,7 @@ export default { ...@@ -152,6 +163,7 @@ export default {
:alt="getAvatarString(activeItem.name)" :alt="getAvatarString(activeItem.name)"
shape="circle" shape="circle"
class="gl-mr-2" class="gl-mr-2"
data-testid="audit-filter-item-avatar"
/> />
{{ activeItemName }} {{ activeItemName }}
</template> </template>
...@@ -164,7 +176,8 @@ export default { ...@@ -164,7 +176,8 @@ export default {
<gl-filtered-search-suggestion <gl-filtered-search-suggestion
v-for="item in suggestions" v-for="item in suggestions"
:key="item.id" :key="item.id"
:value="item.id.toString()" :value="getSuggestionValue(item)"
data-testid="audit-filter-suggestion"
> >
<div class="d-flex"> <div class="d-flex">
<gl-avatar <gl-avatar
......
<script> <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'; import AuditFilterToken from './shared/audit_filter_token.vue';
export default { export default {
...@@ -8,15 +9,25 @@ export default { ...@@ -8,15 +9,25 @@ export default {
}, },
inheritAttrs: false, inheritAttrs: false,
tokenMethods: { tokenMethods: {
fetchItem(id) { fetchItem(term) {
return getUser(id).then((res) => res.data); const username = parseUsername(term);
return getUsers('', { username, per_page: 1 }).then((res) => res.data[0]);
}, },
fetchSuggestions(term) { fetchSuggestions(term) {
return getUsers(term).then((res) => res.data); return getUsers(parseUsername(term)).then((res) => res.data);
}, },
getItemName({ name }) { getItemName({ name }) {
return 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>
......
...@@ -12,7 +12,7 @@ const DEFAULT_TOKEN_OPTIONS = { ...@@ -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 // 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 */ /* eslint-disable @gitlab/require-i18n-strings */
const ENTITY_TYPES = { export const ENTITY_TYPES = {
USER: 'User', USER: 'User',
AUTHOR: 'Author', AUTHOR: 'Author',
GROUP: 'Group', GROUP: 'Group',
......
...@@ -4,14 +4,17 @@ export default { ...@@ -4,14 +4,17 @@ export default {
[types.INITIALIZE_AUDIT_EVENTS]( [types.INITIALIZE_AUDIT_EVENTS](
state, state,
{ {
entity_id: id = null, entity_id: entityId = null,
entity_username: entityUsername = null,
author_username: authorUsername = null,
entity_type: type = null, entity_type: type = null,
created_after: startDate = null, created_after: startDate = null,
created_before: endDate = null, created_before: endDate = null,
sort: sortBy = 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.startDate = startDate;
state.endDate = endDate; state.endDate = endDate;
state.sortBy = sortBy; 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 { 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) => { export const getTypeFromEntityType = (entityType) => {
return AUDIT_FILTER_CONFIGS.find( return AUDIT_FILTER_CONFIGS.find(
...@@ -15,24 +16,45 @@ export const parseAuditEventSearchQuery = ({ ...@@ -15,24 +16,45 @@ export const parseAuditEventSearchQuery = ({
created_after: createdAfter, created_after: createdAfter,
created_before: createdBefore, created_before: createdBefore,
entity_type: entityType, entity_type: entityType,
entity_username: entityUsername,
author_username: authorUsername,
...restOfParams ...restOfParams
}) => ({ }) => ({
...restOfParams, ...restOfParams,
created_after: createdAfter ? parsePikadayDate(createdAfter) : null, created_after: createdAfter ? parsePikadayDate(createdAfter) : null,
created_before: createdBefore ? parsePikadayDate(createdBefore) : null, created_before: createdBefore ? parsePikadayDate(createdBefore) : null,
entity_type: getTypeFromEntityType(entityType), entity_type: getTypeFromEntityType(entityType),
entity_username: displayUsername(entityUsername),
author_username: displayUsername(authorUsername),
}); });
export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, sortBy }) => { export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, sortBy }) => {
const entityValue = filterValue.find((value) => AVAILABLE_TOKEN_TYPES.includes(value.type)); 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_after: startDate ? pikadayToString(startDate) : null,
created_before: endDate ? pikadayToString(endDate) : null, created_before: endDate ? pikadayToString(endDate) : null,
sort: sortBy, sort: sortBy,
entity_id: entityValue?.value.data, entity_type: entityType,
entity_type: getEntityTypeFromType(entityValue?.type), entity_id: null,
entity_username: null,
author_username: null,
// When changing the search parameters, we should be resetting to the first page // When changing the search parameters, we should be resetting to the first page
page: null, page: null,
}; };
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', () => { ...@@ -41,7 +41,7 @@ describe('Audit Event actions', () => {
); );
it('setFilterValue action should commit to the store', () => { 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 }]); testAction(actions.setFilterValue, payload, state, [{ type: types.SET_FILTER_VALUE, payload }]);
}); });
...@@ -91,6 +91,9 @@ describe('Audit Event actions', () => { ...@@ -91,6 +91,9 @@ describe('Audit Event actions', () => {
payload: { payload: {
created_after: null, created_after: null,
created_before: null, created_before: null,
author_username: null,
entity_username: null,
entity_type: undefined,
}, },
}, },
]); ]);
...@@ -100,7 +103,7 @@ describe('Audit Event actions', () => { ...@@ -100,7 +103,7 @@ describe('Audit Event actions', () => {
describe('with a full search query', () => { describe('with a full search query', () => {
beforeEach(() => { beforeEach(() => {
setWindowLocation( 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', () => { ...@@ -112,8 +115,10 @@ describe('Audit Event actions', () => {
created_after: new Date('2020-06-05T00:00:00.000Z'), created_after: new Date('2020-06-05T00:00:00.000Z'),
created_before: new Date('2020-06-25T00:00:00.000Z'), created_before: new Date('2020-06-25T00:00:00.000Z'),
entity_id: '44', entity_id: '44',
entity_type: 'user', entity_type: 'project',
sort: 'created_desc', sort: 'created_desc',
author_username: null,
entity_username: null,
}, },
}, },
]); ]);
......
...@@ -17,7 +17,7 @@ describe('Audit Events getters', () => { ...@@ -17,7 +17,7 @@ describe('Audit Events getters', () => {
describe('with filters and dates', () => { describe('with filters and dates', () => {
it('returns the export url', () => { 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 startDate = new Date(2020, 1, 2);
const endDate = new Date(2020, 1, 30); const endDate = new Date(2020, 1, 30);
const state = { ...createState, ...{ filterValue, startDate, endDate } }; const state = { ...createState, ...{ filterValue, startDate, endDate } };
...@@ -25,7 +25,7 @@ describe('Audit Events getters', () => { ...@@ -25,7 +25,7 @@ describe('Audit Events getters', () => {
expect(getters.buildExportHref(state)(exportUrl)).toEqual( expect(getters.buildExportHref(state)(exportUrl)).toEqual(
'https://example.com/audit_reports.csv?' + 'https://example.com/audit_reports.csv?' +
'created_after=2020-02-02&created_before=2020-03-01' + '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', () => { ...@@ -38,9 +38,13 @@ describe('Audit Event mutations', () => {
sort: 'created_asc', sort: 'created_asc',
}; };
const createFilterValue = (data) => {
return [{ type: payload.entity_type, value: { data, operator: '=' } }];
};
it.each` it.each`
stateKey | expectedState stateKey | expectedState
${'filterValue'} | ${[{ type: payload.entity_type, value: { data: payload.entity_id, operator: '=' } }]} ${'filterValue'} | ${createFilterValue(payload.entity_id)}
${'startDate'} | ${payload.created_after} ${'startDate'} | ${payload.created_after}
${'endDate'} | ${payload.created_before} ${'endDate'} | ${payload.created_before}
${'sortBy'} | ${payload.sort} ${'sortBy'} | ${payload.sort}
...@@ -50,5 +54,21 @@ describe('Audit Event mutations', () => { ...@@ -50,5 +54,21 @@ describe('Audit Event mutations', () => {
expect(state[stateKey]).toEqual(expectedState); 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', () => { ...@@ -34,7 +34,7 @@ describe('Audit Event Utils', () => {
sortBy: 'created_asc', sortBy: 'created_asc',
}; };
expect(parseAuditEventSearchQuery(input)).toEqual({ expect(parseAuditEventSearchQuery(input)).toMatchObject({
created_after: new Date('2020-03-13'), created_after: new Date('2020-03-13'),
created_before: new Date('2020-04-13'), created_before: new Date('2020-04-13'),
sortBy: 'created_asc', sortBy: 'created_asc',
...@@ -43,22 +43,35 @@ describe('Audit Event Utils', () => { ...@@ -43,22 +43,35 @@ describe('Audit Event Utils', () => {
}); });
describe('createAuditEventSearchQuery', () => { describe('createAuditEventSearchQuery', () => {
it('returns a query object with remapped keys and stringified dates', () => { const createFilterParams = (type, data) => ({
const input = { filterValue: [{ type, value: { data, operator: '=' } }],
filterValue: [{ type: 'user', value: { data: '1', operator: '=' } }],
startDate: new Date('2020-03-13'), startDate: new Date('2020-03-13'),
endDate: new Date('2020-04-13'), endDate: new Date('2020-04-13'),
sortBy: 'bar', sortBy: 'bar',
}; });
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({ expect(createAuditEventSearchQuery(input)).toEqual({
entity_id: '1', entity_id,
entity_type: 'User', entity_username,
author_username,
entity_type,
created_after: '2020-03-13', created_after: '2020-03-13',
created_before: '2020-04-13', created_before: '2020-04-13',
sort: 'bar', sort: 'bar',
page: null, 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