Unverified Commit 2ac2dc30 authored by Daniel's avatar Daniel Committed by GitHub

Update Dependencies based on Security Issues (#354)

* Update dependencies to resolve security issues
* Fixes `getDerivedStateFromProps` usage
* Refactor TagsList behavior, fix tests
parent dbb33e1d
......@@ -34,7 +34,6 @@ module.exports = {
transform: {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.js$': 'babel-jest',
'^.+\\.(css|scss)$': '<rootDir>/node_modules/jest-css-modules',
},
testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.(j|t)sx?$',
moduleDirectories: ['node_modules', 'js'],
......@@ -45,6 +44,9 @@ module.exports = {
'jsx',
'json',
],
moduleNameMapper: {
'^.+\\.(css|scss)$': '<rootDir>/node_modules/jest-css-modules',
},
globals: {
'ts-jest': {
diagnostics: false,
......
......@@ -2,7 +2,7 @@ import * as React from 'react';
import * as DocumentTitle from 'react-document-title';
import SanitizedHTML from 'react-sanitized-html';
import { mount, shallow } from 'enzyme';
import { shallow } from 'enzyme';
import { AnnouncementPage, AnnouncementPageProps, mapDispatchToProps, mapStateToProps } from '../';
......@@ -26,7 +26,7 @@ describe('AnnouncementPage', () => {
html_content: '<div>Just kidding</div>',
}],
};
subject = mount(<AnnouncementPage {...props} />);
subject = shallow(<AnnouncementPage {...props} />);
});
describe('componentDidMount', () => {
......
......@@ -25,7 +25,6 @@ export interface DispatchFromProps {
export interface ComponentProps {
errorText?: string | null;
readOnly: boolean;
}
interface OwnerAvatarLabelProps extends AvatarLabelProps {
......@@ -42,7 +41,6 @@ type OwnerEditorProps = ComponentProps & DispatchFromProps & StateFromProps & Ed
interface OwnerEditorState {
errorText: string | null;
isLoading: boolean;
itemProps: { [id: string]: OwnerAvatarLabelProps };
readOnly: boolean;
tempItemProps: { [id: string]: AvatarLabelProps };
......@@ -56,20 +54,13 @@ export class OwnerEditor extends React.Component<OwnerEditorProps, OwnerEditorSt
isLoading: false,
itemProps: {},
onUpdateList: () => undefined,
readOnly: true,
};
static getDerivedStateFromProps(nextProps) {
const { isLoading, itemProps, readOnly } = nextProps;
return { isLoading, itemProps, readOnly, tempItemProps: itemProps };
}
constructor(props) {
super(props);
this.state = {
errorText: props.errorText,
isLoading: props.isLoading,
itemProps: props.itemProps,
readOnly: props.readOnly,
tempItemProps: props.itemProps,
......@@ -78,6 +69,13 @@ export class OwnerEditor extends React.Component<OwnerEditorProps, OwnerEditorSt
this.inputRef = React.createRef();
}
componentDidUpdate(prevProps) {
// TODO - itemProps is a new object and this check needs to be fixed
if (prevProps.itemProps !== this.props.itemProps) {
this.setState({ itemProps: this.props.itemProps, tempItemProps: this.props.itemProps });
}
}
handleShow = () => {
this.props.setEditMode(true)
};
......@@ -140,7 +138,7 @@ export class OwnerEditor extends React.Component<OwnerEditorProps, OwnerEditorSt
return null;
}
if (this.state.isLoading) {
if (this.props.isLoading) {
return (
<Modal.Body>
<LoadingSpinner/>
......
......@@ -177,7 +177,7 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps
</EditableSection>
<EditableSection title="Owners">
<OwnerEditor readOnly={false}/>
<OwnerEditor />
</EditableSection>
</section>
......
......@@ -69,22 +69,23 @@ export class SearchBar extends React.Component<SearchBarProps, SearchBarState> {
};
}
static getDerivedStateFromProps(props, state) {
const { searchTerm } = props;
return { searchTerm };
}
clearSearchTerm = () : void => {
this.setState({ showTypeAhead: false, searchTerm: '' });
};
componentDidMount = () => {
document.addEventListener('mousedown', this.updateTypeAhead, false);
}
};
componentWillUnmount = () => {
document.removeEventListener('mousedown', this.updateTypeAhead, false);
}
};
componentDidUpdate = (prevProps: SearchBarProps) => {
if (this.props.searchTerm !== prevProps.searchTerm) {
this.setState({ searchTerm: this.props.searchTerm });
}
};
handleValueChange = (event: React.SyntheticEvent<HTMLInputElement>) : void => {
const searchTerm = (event.target as HTMLInputElement).value.toLowerCase();
......@@ -107,7 +108,7 @@ export class SearchBar extends React.Component<SearchBarProps, SearchBarState> {
hideTypeAhead = () : void => {
this.setState({ showTypeAhead: false });
}
};
isFormValid = (searchTerm: string) : boolean => {
if (searchTerm.length === 0) {
......
......@@ -50,20 +50,6 @@ describe('SearchBar', () => {
});
});
describe('getDerivedStateFromProps', () => {
it('sets searchTerm on state from props', () => {
const { props, wrapper } = setup();
const prevState = wrapper.state();
props.searchTerm = 'newTerm';
// @ts-ignore: Why does this work in other tests but complain here
wrapper.setProps(props);
expect(wrapper.state()).toMatchObject({
...prevState,
searchTerm: 'newTerm',
});
});
});
describe('clearSearchTerm', () => {
it('sets the searchTerm to an empty string', () => {
setStateSpy.mockClear();
......@@ -90,6 +76,20 @@ describe('SearchBar', () => {
});
});
describe('componentDidUpdate', () => {
it('sets the searchTerm state when props update', () => {
const { props, wrapper } = setup();
const prevState = wrapper.state();
props.searchTerm = 'newTerm';
// @ts-ignore: Why does this work in other tests but complain here
wrapper.setProps(props);
expect(wrapper.state()).toMatchObject({
...prevState,
searchTerm: 'newTerm',
});
});
});
describe('handleValueChange', () => {
let shouldShowTypeAheadSpy;
......
......@@ -4,7 +4,6 @@ import { bindActionCreators } from 'redux';
import './styles.scss';
import AppConfig from 'config/config';
import LoadingSpinner from 'components/common/LoadingSpinner';
import TagInfo from 'components/Tags/TagInfo';
import { Tag } from 'interfaces';
......@@ -12,9 +11,11 @@ import { Tag } from 'interfaces';
import { GlobalState } from 'ducks/rootReducer';
import { getAllTags } from 'ducks/allTags/reducer';
import { GetAllTagsRequest } from 'ducks/allTags/types';
import { getCuratedTags, showAllTags } from 'config/config-utils';
export interface StateFromProps {
allTags: Tag[];
curatedTags: Tag[];
otherTags: Tag[];
isLoading: boolean;
}
......@@ -22,40 +23,11 @@ export interface DispatchFromProps {
getAllTags: () => GetAllTagsRequest;
}
interface TagsListState {
curatedTags: Tag[];
otherTags: Tag[];
}
export type TagsListProps = StateFromProps & DispatchFromProps;
export class TagsList extends React.Component<TagsListProps, TagsListState> {
export class TagsList extends React.Component<TagsListProps> {
constructor(props) {
super(props);
this.state = {
curatedTags: [],
otherTags: [],
};
}
static getDerivedStateFromProps(nextProps, prevState) {
const { allTags, isLoading } = nextProps;
if (isLoading) {
return {
...prevState,
isLoading,
};
}
const curatedTagsList = AppConfig.browse.curatedTags;
const curatedTags = allTags.filter((tag) => curatedTagsList.indexOf(tag.tag_name) !== -1);
let otherTags = [];
if (AppConfig.browse.showAllTags) {
otherTags = allTags.filter((tag) => curatedTagsList.indexOf(tag.tag_name) === -1);
}
return { curatedTags, otherTags, isLoading };
}
componentDidMount() {
......@@ -68,28 +40,34 @@ export class TagsList extends React.Component<TagsListProps, TagsListState> {
}
render() {
let innerContent;
if (this.props.isLoading) {
innerContent = <LoadingSpinner/>;
} else {
innerContent = (
<div id="browse-body" className="browse-body">
{this.generateTagInfo(this.state.curatedTags)}
{
this.state.curatedTags.length > 0 && this.state.otherTags.length > 0 &&
<hr />
}
{this.generateTagInfo(this.state.otherTags)}
</div>
);
return <LoadingSpinner/>;
}
return (innerContent);
return (
<div id="tags-list" className="tags-list">
{ this.generateTagInfo(this.props.curatedTags) }
{
showAllTags() && this.props.curatedTags.length > 0 && this.props.otherTags.length > 0 &&
<hr />
}
{
showAllTags() && this.props.otherTags.length > 0 &&
this.generateTagInfo(this.props.otherTags)
}
</div>
);
}
}
export const mapStateToProps = (state: GlobalState) => {
const curatedTagsList = getCuratedTags();
const allTags = state.allTags.allTags;
const curatedTags = allTags.filter((tag) => curatedTagsList.indexOf(tag.tag_name) !== -1);
const otherTags = allTags.filter((tag) => curatedTagsList.indexOf(tag.tag_name) === -1);
return {
allTags: state.allTags.allTags,
curatedTags,
otherTags,
isLoading: state.allTags.isLoading,
};
};
......
......@@ -4,6 +4,6 @@ hr.header-hr {
border: 2px solid $brand-color-4;
}
.browse-body {
.tags-list {
margin: 0 -4px;
}
......@@ -10,17 +10,27 @@ import globalState from 'fixtures/globalState';
import AppConfig from 'config/config';
AppConfig.browse.curatedTags = ['test1'];
jest.mock('config/config-utils', () => (
{
showAllTags: jest.fn(),
getCuratedTags: () => { return ['curated_tag_1']; },
}
));
import { getCuratedTags, showAllTags } from 'config/config-utils';
describe('TagsList', () => {
let props: TagsListProps;
let subject;
beforeEach(() => {
props = {
allTags: [
const setup = (propOverrides?: Partial<TagsListProps>) => {
const props: TagsListProps = {
curatedTags: [
{
tag_count: 2,
tag_name: 'test1',
},
],
otherTags: [
{
tag_count: 1,
tag_name: 'test2',
......@@ -28,78 +38,58 @@ describe('TagsList', () => {
],
isLoading: false,
getAllTags: jest.fn(),
...propOverrides,
};
subject = shallow(<TagsList {...props} />);
});
describe('getDerivedStateFromProps', () => {
it('returns correct state if props.isLoading', () => {
const prevState = subject.state();
props.isLoading = true;
subject.setProps(props);
expect(subject.state()).toMatchObject({
...prevState,
isLoading: true,
});
});
it('returns correct state if !props.isLoading', () => {
props.isLoading = false;
subject.setProps(props);
expect(subject.state()).toMatchObject({
curatedTags: [{ tag_count: 2, tag_name: 'test1'}],
otherTags: [{ tag_count: 1, tag_name: 'test2'}],
isLoading: false,
});
});
it('returns correct state if !props.isLoading and !AppConfig.browse.showAllTags', () => {
AppConfig.browse.showAllTags = false;
subject = shallow(<TagsList {...props} />);
expect(subject.state()).toMatchObject({
curatedTags: [{tag_count: 2, tag_name: 'test1'}],
otherTags: [],
isLoading: false,
});
AppConfig.browse.showAllTags = true; // reset so other tests aren't affected
});
});
const wrapper = shallow(<TagsList {...props} />);
return { props, wrapper };
};
describe('componentDidMount', () => {
it('calls props.getAllTags', () => {
expect(props.getAllTags).toHaveBeenCalled();
});
it('calls props.getAllTags', () => {
const { props, wrapper } = setup();
expect(props.getAllTags).toHaveBeenCalled();
});
});
describe('render', () => {
let spy;
beforeEach(() => {
spy = jest.spyOn(TagsList.prototype, 'generateTagInfo');
it('renders LoadingSpinner if props.isLoading is true', () => {
const { props, wrapper } = setup({ isLoading: true });
expect(wrapper.find(LoadingSpinner).exists()).toBe(true);
});
it('renders LoadingSpinner if state.isLoading', () => {
/* Note: For some reason setState is not updating the component in this case */
props.isLoading = true;
subject.setProps(props);
expect(subject.find(LoadingSpinner).exists()).toBeTruthy();
it('renders <hr> if curatedTags.length > 0 & otherTags.length > 0 & showAllTags == true', () => {
// @ts-ignore
showAllTags.mockImplementation(() => true);
const { props, wrapper } = setup();
expect(wrapper.find('hr').exists()).toBe(true);
});
it('renders <hr> in if curatedTags.length > 0 & otherTags.length > 0 ', () => {
expect(subject.find('#browse-body').find('hr').exists()).toBeTruthy();
it('does not render <hr> if showAllTags is false', () => {
// @ts-ignore
showAllTags.mockImplementation(() => false);
const { props, wrapper } = setup();
expect(wrapper.find('hr').exists()).toBe(false);
});
it('does not render <hr> if !(curatedTags.length > 0 & otherTags.length > 0) ', () => {
AppConfig.browse.curatedTags = ['test1', 'test2'];
subject = shallow(<TagsList {...props} />);
expect(subject.find('#browse-body').find('hr').exists()).toBeFalsy();
AppConfig.browse.curatedTags = ['test1']; // reset so other tests aren't affected
it('does not render an <hr> if otherTags is empty', () => {
// @ts-ignore
showAllTags.mockImplementation(() => true);
const { props, wrapper } = setup();
expect(wrapper.find('#tags-list').find('hr').exists()).toBe(true);
});
it('calls generateTagInfo with curatedTags', () => {
expect(spy).toHaveBeenCalledWith(subject.state().curatedTags);
const generateTagInfoSpy = jest.spyOn(TagsList.prototype, 'generateTagInfo');
const { props, wrapper } = setup();
expect(generateTagInfoSpy).toHaveBeenCalledWith(props.curatedTags)
});
it('call generateTagInfo with otherTags', () => {
expect(spy).toHaveBeenCalledWith(subject.state().otherTags);
const generateTagInfoSpy = jest.spyOn(TagsList.prototype, 'generateTagInfo');
const { props, wrapper } = setup();
expect(generateTagInfoSpy).toHaveBeenCalledWith(props.otherTags)
});
});
});
......@@ -120,12 +110,22 @@ describe('mapDispatchToProps', () => {
describe('mapStateToProps', () => {
let result;
let expectedCuratedTags;
let expectedOtherTags;
beforeEach(() => {
result = mapStateToProps(globalState);
const allTags = globalState.allTags.allTags;
const curatedTagsList = getCuratedTags();
expectedCuratedTags = allTags.filter((tag) => curatedTagsList.indexOf(tag.tag_name) !== -1);
expectedOtherTags = allTags.filter((tag) => curatedTagsList.indexOf(tag.tag_name) === -1);
});
it('sets curatedTags on the props', () => {
expect(result.curatedTags).toEqual(expectedCuratedTags);
});
it('sets allTags on the props', () => {
expect(result.allTags).toEqual(globalState.allTags.allTags);
it('sets otherTags on the props', () => {
expect(result.otherTags).toEqual(expectedOtherTags);
});
it('sets isLoading on the props', () => {
......
......@@ -67,3 +67,17 @@ export function indexUsersEnabled(): boolean {
export function notificationsEnabled(): boolean {
return AppConfig.mailClientFeatures.notificationsEnabled;
}
/**
* Returns whether or not to show all tags
*/
export function showAllTags(): boolean {
return AppConfig.browse.showAllTags;
}
/**
* Returns a list of curated tag names
*/
export function getCuratedTags(): string[] {
return AppConfig.browse.curatedTags;
}
......@@ -72,3 +72,19 @@ describe('notificationsEnabled', () => {
expect(ConfigUtils.notificationsEnabled()).toBe(AppConfig.mailClientFeatures.notificationsEnabled);
});
});
describe('showAllTags', () => {
it('returns whether or not to show all tags', () => {
AppConfig.browse.showAllTags = true;
expect(ConfigUtils.showAllTags()).toBe(AppConfig.browse.showAllTags);
AppConfig.browse.showAllTags = false;
expect(ConfigUtils.showAllTags()).toBe(AppConfig.browse.showAllTags);
});
});
describe('getCuratedTags', () => {
it('returns a list of curated tags', () => {
AppConfig.browse.curatedTags = ['one', 'two', 'three'];
expect(ConfigUtils.getCuratedTags()).toBe(AppConfig.browse.curatedTags);
});
});
......@@ -7,10 +7,10 @@ export function getAllTags(): GetAllTagsRequest {
return { type: GetAllTags.REQUEST };
};
export function getAllTagsFailure(): GetAllTagsResponse {
return { type: GetAllTags.FAILURE, payload: { tags: [] } };
return { type: GetAllTags.FAILURE, payload: { allTags: [] } };
};
export function getAllTagsSuccess(tags: Tag[]): GetAllTagsResponse {
return { type: GetAllTags.SUCCESS, payload: { tags } };
export function getAllTagsSuccess(allTags: Tag[]): GetAllTagsResponse {
return { type: GetAllTags.SUCCESS, payload: { allTags } };
};
/* REDUCER */
......@@ -31,7 +31,11 @@ export default function reducer(state: AllTagsReducerState = initialState, actio
case GetAllTags.FAILURE:
return initialState;
case GetAllTags.SUCCESS:
return { ...state, allTags: (<GetAllTagsResponse>action).payload.tags, isLoading: false };
return {
...state,
allTags: (<GetAllTagsResponse>action).payload.allTags,
isLoading: false,
};
default:
return state;
}
......
......@@ -7,8 +7,9 @@ import { GetAllTags } from './types';
export function* getAllTagsWorker(): SagaIterator {
try {
const tags = yield call(API.getAllTags);
yield put(getAllTagsSuccess(tags));
const allTags = yield call(API.getAllTags);
yield put(getAllTagsSuccess(allTags));
} catch (e) {
yield put(getAllTagsFailure());
}
......
......@@ -21,7 +21,7 @@ describe('allTags ducks', () => {
const action = getAllTagsFailure();
const { payload } = action;
expect(action.type).toBe(GetAllTags.FAILURE);
expect(payload.tags).toEqual([]);
expect(payload.allTags).toEqual([]);
});
it('getAllTagsSuccess - returns the action to process success', () => {
......@@ -29,7 +29,7 @@ describe('allTags ducks', () => {
const action = getAllTagsSuccess(expectedTags);
const { payload } = action;
expect(action.type).toBe(GetAllTags.SUCCESS);
expect(payload.tags).toBe(expectedTags);
expect(payload.allTags).toBe(expectedTags);
});
});
......
......@@ -11,6 +11,6 @@ export interface GetAllTagsRequest {
export interface GetAllTagsResponse {
type: GetAllTags.SUCCESS | GetAllTags.FAILURE;
payload: {
tags: Tag[];
allTags: Tag[];
};
}
import { GlobalState } from 'ducks/rootReducer';
import { RequestMetadataType, ResourceType, SendingState } from 'interfaces';
import { ResourceType, SendingState } from 'interfaces';
const globalState: GlobalState = {
announcements: {
......@@ -135,7 +135,16 @@ const globalState: GlobalState = {
},
},
allTags: {
allTags: [],
allTags: [
{
tag_name: 'curated_tag_1',
tag_count: 20,
},
{
tag_name: 'other_tag_1',
tag_count: 15,
}
],
isLoading: false,
},
user: {
......
......@@ -25,9 +25,9 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@types/enzyme": "^3.1.14",
"@types/enzyme": "^3.10.3",
"@types/jasmine-matchers": "^0.2.31",
"@types/jest": "^23.3.9",
"@types/jest": "^24.0.23",
"@types/node": "^10.12.10",
"@types/react-redux": "^6.0.0",
"@types/react-router": "^4.0.25",
......@@ -35,19 +35,19 @@
"@types/webpack": "^4.4.19",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-jest": "^23.6.0",
"babel-jest": "^24.9.0",
"babel-loader": "^7.1.4",
"babel-polyfill": "^6.0.16",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.0.15",
"bootstrap-sass": "^3.3.7",
"bootstrap-sass": "^3.4.1",
"clean-webpack-plugin": "^0.1.19",
"cross-env": "^5.2.1",
"css-loader": "^0.28.11",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.6.0",
"css-loader": "^3.2.0",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.15.1",
"enzyme-to-json": "^3.3.4",
"eslint": "^4.9.0",
"eslint-config-airbnb": "^16.1.0",
......@@ -55,8 +55,8 @@
"eslint-plugin-jsx-a11y": "^6.0.2",
"eslint-plugin-react": "^7.4.0",
"html-webpack-plugin": "^3.2.0",
"jest": "^24.0.0",
"jest-css-modules": "^1.1.0",
"jest": "^24.9.0",
"jest-css-modules": "^2.1.0",
"mini-css-extract-plugin": "^0.4.5",
"node-sass": "^4.10.0",
"postcss": "^7.0.6",
......@@ -67,18 +67,18 @@
"sass-loader": "^7.1.0",
"style-loader": "^0.20.3",
"terser-webpack-plugin": "^1.1.0",
"ts-jest": "^24.0.0",
"ts-loader": "4.0.0",
"ts-node": "^7.0.1",
"tsconfig-paths": "^3.7.0",
"tslint": "^5.10.0",
"ts-jest": "^24.1.0",
"ts-loader": "^6.2.1",
"ts-node": "^8.5.2",
"tsconfig-paths": "^3.9.0",
"tslint": "^5.20.1",
"tslint-config-airbnb": "^5.8.0",
"tslint-config-prettier": "^1.12.0",
"tslint-eslint-rules": "^5.2.0",
"tslint-react": "^3.6.0",
"typescript": "^3.1.1",
"uglifyjs-webpack-plugin": "^1.1.0",
"webpack": "^4.19.0",
"webpack": "^4.41.3",
"webpack-cli": "^3.1.1",
"webpack-merge": "^4.1.4",
"webpack-sources": "^1.1.0",
......@@ -88,13 +88,13 @@
"autosize": "^4.0.2",
"axios": "0.19.0",
"form-serialize": "^0.7.2",
"jquery": "^3.3.1",
"jquery": "^3.4.0",
"moment-timezone": "^0.5.21",
"react": "^16.3.1",
"react": "^16.3.3",
"react-avatar": "^2.5.1",
"react-bootstrap": "^0.32.1",
"react-document-title": "^2.0.3",
"react-dom": "^16.3.1",
"react-dom": "^16.3.3",
"react-js-pagination": "^3.0.2",
"react-linkify": "^0.2.2",
"react-markdown": "^4.2.2",
......
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