diff --git a/test/integration/host_js_test.rb b/test/integration/host_js_test.rb index 7072ad49bb..d939598bb6 100644 --- a/test/integration/host_js_test.rb +++ b/test/integration/host_js_test.rb @@ -123,7 +123,10 @@ class HostJSTest < IntegrationTestWithJavascript test "all audit redirect to audit page" do visit host_details_page_path(@host) find('a', :text => /All audits/).click - assert_current_path audits_path(search: "host=#{@host.fqdn}") + assert_current_path audits_path, ignore_query: true + query = Rack::Utils.parse_query(URI.parse(current_url).query) + assert_equal "host=#{@host.fqdn}", query['search'] + assert_equal '1', query['page'] end test "manage host statuses modal" do diff --git a/test/integration/hostgroup_js_test.rb b/test/integration/hostgroup_js_test.rb index 1ef4dca91a..fc18746197 100644 --- a/test/integration/hostgroup_js_test.rb +++ b/test/integration/hostgroup_js_test.rb @@ -21,6 +21,7 @@ class HostgroupJSTest < IntegrationTestWithJavascript select2 os.media.first.name, :from => 'hostgroup_medium_id' wait_for_ajax select2 os.ptables.first.name, :from => 'hostgroup_ptable_id' + wait_for_ajax fill_in 'hostgroup_root_pass', :with => '12345678' click_button 'Submit' hostgroup = wait_for do diff --git a/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/AuditsPageActions.js b/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/AuditsPageActions.js index e4fa821002..ae7887a49c 100644 --- a/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/AuditsPageActions.js +++ b/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/AuditsPageActions.js @@ -23,7 +23,8 @@ import { translate as __ } from '../../../common/I18n'; export const initializeAudits = () => dispatch => { const params = getParams(); dispatch(fetchAudits(params)); - if (!history.action === 'POP') { + + if (history.action !== 'POP') { history.replace({ pathname: AUDITS_PATH, search: stringifyParams(params), @@ -36,14 +37,17 @@ export const fetchAudits = ( url = AUDITS_PATH ) => async (dispatch, getState) => { dispatch({ type: AUDITS_PAGE_SHOW_LOADING }); - if (selectAuditsHasError(getState())) + + if (selectAuditsHasError(getState())) { dispatch({ type: AUDITS_PAGE_CLEAR_ERROR, }); + } const onRequestSuccess = ({ data: { audits, itemCount } }) => { - if (selectAuditsIsLoadingPage(getState())) + if (selectAuditsIsLoadingPage(getState())) { dispatch({ type: AUDITS_PAGE_HIDE_LOADING }); + } dispatch({ type: AUDITS_PAGE_UPDATE_QUERY, @@ -63,9 +67,11 @@ export const fetchAudits = ( }, }); }; + const onRequestFail = error => { - if (selectAuditsIsLoadingPage(getState())) + if (selectAuditsIsLoadingPage(getState())) { dispatch({ type: AUDITS_PAGE_HIDE_LOADING }); + } dispatch({ type: AUDITS_PAGE_DATA_FAILED, @@ -77,6 +83,7 @@ export const fetchAudits = ( }, }); }; + try { const response = await API.get( url, @@ -87,6 +94,7 @@ export const fetchAudits = ( search: searchQuery, } ); + return onRequestSuccess(response); } catch (error) { return onRequestFail(error); diff --git a/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/AuditsPageActions.test.js b/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/AuditsPageActions.test.js index bf7770ca86..7c08b5195c 100644 --- a/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/AuditsPageActions.test.js +++ b/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/AuditsPageActions.test.js @@ -1,5 +1,15 @@ import { API } from '../../../../redux/API'; -import { testActionSnapshotWithFixtures } from '../../../../common/testHelpers'; +import history from '../../../../history'; +import { getParams, stringifyParams } from '../../../../common/urlHelpers'; +import { + AUDITS_PAGE_CLEAR_ERROR, + AUDITS_PAGE_DATA_FAILED, + AUDITS_PAGE_DATA_RESOLVED, + AUDITS_PAGE_HIDE_LOADING, + AUDITS_PAGE_SHOW_LOADING, + AUDITS_PAGE_UPDATE_QUERY, + AUDITS_PATH, +} from '../../constants'; import { fetchAudits, fetchAndPush, @@ -13,55 +23,350 @@ import { } from '../AuditsPage.fixtures'; jest.mock('../../../../redux/API/API'); +jest.mock('../../../../history', () => ({ + __esModule: true, + default: { + push: jest.fn(), + replace: jest.fn(), + action: 'PUSH', + }, +})); +jest.mock('../../../../common/urlHelpers', () => ({ + ...jest.requireActual('../../../../common/urlHelpers'), + getParams: jest.fn(), +})); + +const createDispatch = getState => { + const pendingPromises = []; + + const dispatch = jest.fn(action => { + if (typeof action === 'function') { + const result = action(dispatch, getState); + + if (result?.then) { + pendingPromises.push(result); + } + + return result; + } -const runWithGetState = (state, action, ...params) => async dispatch => { - const getState = () => ({ - auditsPage: state, + return action; }); - await action(...params)(dispatch, getState); + + dispatch.flush = () => Promise.all(pendingPromises); + + return dispatch; +}; + +const getDispatchedActions = dispatch => + dispatch.mock.calls + .filter(([action]) => typeof action !== 'function') + .map(([action]) => action); + +const createThunkTestHarness = auditsPageState => { + const getState = () => ({ auditsPage: auditsPageState }); + const dispatch = createDispatch(getState); + + return { + dispatch, + getState, + getActions: () => getDispatchedActions(dispatch), + runThunk: async thunk => { + await thunk(dispatch, getState); + }, + runAndFlush: async actionCreator => { + actionCreator(dispatch, getState); + await dispatch.flush(); + }, + }; +}; + +const showLoadingAction = () => ({ type: AUDITS_PAGE_SHOW_LOADING }); +const clearErrorAction = () => ({ type: AUDITS_PAGE_CLEAR_ERROR }); +const hideLoadingAction = () => ({ type: AUDITS_PAGE_HIDE_LOADING }); + +const updateQueryAction = (query, itemCount) => ({ + type: AUDITS_PAGE_UPDATE_QUERY, + payload: { + page: query.page, + perPage: query.perPage, + searchQuery: query.searchQuery, + itemCount, + }, +}); + +const dataResolvedAction = (audits, hasData) => ({ + type: AUDITS_PAGE_DATA_RESOLVED, + payload: { audits, hasData }, +}); + +const dataFailedAction = (text = 'some-status some status text') => ({ + type: AUDITS_PAGE_DATA_FAILED, + payload: { + message: { type: 'error', text }, + }, +}); + +const buildFetchSuccessActions = (query, response, { prefix = [] } = {}) => { + const { audits, itemCount } = response.data; + + return [ + showLoadingAction(), + ...prefix, + updateQueryAction(query, itemCount), + dataResolvedAction(audits, itemCount > 0), + ]; }; -const runFetchAuditsAPI = (state, resourceMock, serverMock) => { - API.get.mockImplementation(serverMock); +const buildFetchFailureActions = ({ prefix = [] } = {}) => [ + showLoadingAction(), + ...prefix, + dataFailedAction(), +]; + +const runFetchAudits = async (state, query, apiMock) => { + API.get.mockImplementation(apiMock); + const harness = createThunkTestHarness(state); + + await harness.runThunk(fetchAudits(query)); - return runWithGetState(state, fetchAudits, resourceMock); + return { dispatch: harness.dispatch, actions: harness.getActions() }; }; -const fixtures = { - 'should fetch Audits': () => - runFetchAuditsAPI(stateMock.auditsPage, getMock, async () => responseMock), +const apiError = () => { + const error = new Error('some-error'); + error.response = { + status: 'some-status', + statusText: 'some status text', + }; + + throw error; +}; + +const loadingState = { + ...stateMock.auditsPage, + data: { + ...stateMock.auditsPage.data, + isLoading: true, + }, +}; + +describe('AuditsPage actions', () => { + beforeEach(() => { + jest.clearAllMocks(); + history.action = 'PUSH'; + getParams.mockReturnValue({ + page: 1, + perPage: 20, + searchQuery: 'search', + }); + }); - 'should fetch empty Audits': () => - runFetchAuditsAPI( + it('should fetch Audits', async () => { + const { actions } = await runFetchAudits( stateMock.auditsPage, - { ...getMock, searchQuery: 'no-such-audit' }, + getMock, + async () => responseMock + ); + + expect(API.get).toHaveBeenCalledWith( + AUDITS_PATH, + {}, + { + page: getMock.page, + per_page: getMock.perPage, + search: getMock.searchQuery, + } + ); + expect(actions).toEqual(buildFetchSuccessActions(getMock, responseMock)); + }); + + it('should fetch empty Audits', async () => { + const query = { ...getMock, searchQuery: 'no-such-audit' }; + const { actions } = await runFetchAudits( + stateMock.auditsPage, + query, async () => emptyResponseMock - ), - - 'should fetch Audits and remove emptyState': () => - runFetchAuditsAPI(stateMock.auditsPage, getMock, async () => responseMock), - - 'should fetch Audits and fail': () => - runFetchAuditsAPI(stateMock.auditsPage, getMock, async () => { - const error = new Error('some-error'); - error.response = { - status: 'some-status', - statusText: 'some status text', - }; - throw error; - }), - - 'should fetchAndPush': () => - runWithGetState( + ); + + expect(API.get).toHaveBeenCalledWith( + AUDITS_PATH, + {}, { - ...stateMock.auditsPage, - query: { page: 1, perPage: 20, searchQuery: 'search' }, + page: query.page, + per_page: query.perPage, + search: query.searchQuery, + } + ); + expect(actions).toEqual(buildFetchSuccessActions(query, emptyResponseMock)); + }); + + it('should fetch Audits and remove emptyState', async () => { + const stateWithError = { + ...stateMock.auditsPage, + data: { + ...stateMock.auditsPage.data, + hasError: true, }, - fetchAndPush, - getMock - ), - 'should initializeAudits': () => - runWithGetState({ searchQuery: 'search' }, initializeAudits, {}), -}; + }; + const { actions } = await runFetchAudits( + stateWithError, + getMock, + async () => responseMock + ); + + expect(actions).toEqual( + buildFetchSuccessActions(getMock, responseMock, { + prefix: [clearErrorAction()], + }) + ); + }); + + it('should hide loading after successful fetch when page was loading', async () => { + const { actions } = await runFetchAudits( + loadingState, + getMock, + async () => responseMock + ); + + expect(actions).toEqual( + buildFetchSuccessActions(getMock, responseMock, { + prefix: [hideLoadingAction()], + }) + ); + }); + + it('should hide loading after failed fetch when page was loading', async () => { + const { actions } = await runFetchAudits( + loadingState, + getMock, + async () => apiError() + ); + + expect(actions).toEqual( + buildFetchFailureActions({ prefix: [hideLoadingAction()] }) + ); + }); + + it('should clear error and fail when refetching after error', async () => { + const stateWithError = { + ...stateMock.auditsPage, + data: { + ...stateMock.auditsPage.data, + hasError: true, + }, + }; + const { actions } = await runFetchAudits( + stateWithError, + getMock, + async () => apiError() + ); + + expect(actions).toEqual( + buildFetchFailureActions({ prefix: [clearErrorAction()] }) + ); + }); + + it('should fetch Audits and fail', async () => { + const { actions } = await runFetchAudits( + stateMock.auditsPage, + getMock, + async () => apiError() + ); + + expect(actions).toEqual(buildFetchFailureActions()); + }); -describe('AuditsPage actions', () => testActionSnapshotWithFixtures(fixtures)); + it('should fetchAndPush using query defaults from state', async () => { + API.get.mockImplementation(async () => responseMock); + const auditsPageState = { + ...stateMock.auditsPage, + query: { page: 3, perPage: 50, searchQuery: 'from-state', itemCount: 0 }, + }; + const expectedQuery = { + page: 3, + perPage: 50, + searchQuery: 'from-state', + }; + const harness = createThunkTestHarness(auditsPageState); + + await harness.runAndFlush(fetchAndPush({})); + + expect(history.push).toHaveBeenCalledWith({ + pathname: AUDITS_PATH, + search: stringifyParams(expectedQuery), + }); + expect(API.get).toHaveBeenCalledWith( + AUDITS_PATH, + {}, + { + page: expectedQuery.page, + per_page: expectedQuery.perPage, + search: expectedQuery.searchQuery, + } + ); + }); + + it('should fetchAndPush', async () => { + API.get.mockImplementation(async () => responseMock); + const auditsPageState = { + ...stateMock.auditsPage, + query: { page: 1, perPage: 20, searchQuery: 'search' }, + }; + const expectedQuery = { + page: getMock.page, + perPage: getMock.perPage, + searchQuery: getMock.searchQuery, + }; + const harness = createThunkTestHarness(auditsPageState); + + await harness.runAndFlush(fetchAndPush(getMock)); + + expect(history.push).toHaveBeenCalledWith({ + pathname: AUDITS_PATH, + search: stringifyParams(expectedQuery), + }); + expect(harness.getActions()).toEqual( + buildFetchSuccessActions(expectedQuery, responseMock) + ); + }); + + it('should initializeAudits without replacing history on POP', async () => { + API.get.mockImplementation(async () => responseMock); + history.action = 'POP'; + const params = { + page: 1, + perPage: 20, + searchQuery: 'search', + }; + getParams.mockReturnValue(params); + const harness = createThunkTestHarness(stateMock.auditsPage); + + await harness.runAndFlush(initializeAudits()); + + expect(getParams).toHaveBeenCalled(); + expect(history.replace).not.toHaveBeenCalled(); + }); + + it('should initializeAudits', async () => { + API.get.mockImplementation(async () => responseMock); + const params = { + page: 1, + perPage: 20, + searchQuery: 'search', + }; + getParams.mockReturnValue(params); + const harness = createThunkTestHarness(stateMock.auditsPage); + + await harness.runAndFlush(initializeAudits()); + + expect(getParams).toHaveBeenCalled(); + expect(history.replace).toHaveBeenCalledWith({ + pathname: AUDITS_PATH, + search: stringifyParams(params), + }); + expect(harness.getActions()).toEqual( + buildFetchSuccessActions(params, responseMock) + ); + }); +}); diff --git a/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/__snapshots__/AuditsPageActions.test.js.snap b/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/__snapshots__/AuditsPageActions.test.js.snap deleted file mode 100644 index 9a9500cef0..0000000000 --- a/webpack/assets/javascripts/react_app/routes/Audits/AuditsPage/__tests__/__snapshots__/AuditsPageActions.test.js.snap +++ /dev/null @@ -1,332 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AuditsPage actions should fetch Audits 1`] = ` -Array [ - Array [ - Object { - "type": "AUDITS_PAGE_SHOW_LOADING", - }, - ], - Array [ - Object { - "payload": Object { - "itemCount": 1, - "page": 1, - "perPage": 20, - "searchQuery": "", - }, - "type": "AUDITS_PAGE_UPDATE_QUERY", - }, - ], - Array [ - Object { - "payload": Object { - "audits": Array [ - Object { - "action": "update", - "action_display_name": "updated", - "affected_locations": Array [ - Object { - "name": "test_loc1", - "url": "/locations/2-test_loc1/edit", - }, - Object { - "name": "test_loc2", - "url": "/locations/6-test_loc2/edit", - }, - Object { - "name": "test_loc3", - "url": "/locations/9-test_loc3/edit", - }, - ], - "affected_organizations": Array [ - Object { - "name": "test_org1", - "url": "/organizations/1-test_org1/edit", - }, - Object { - "name": "test_org2", - "url": "/organizations/3-test_org2/edit", - }, - Object { - "name": "test_org3", - "url": "/organizations/5-test_org3/edit", - }, - ], - "allowed_actions": Array [ - Object { - "css_class": "pf-v5-c-button pf-m-primary pf-m-small pf-v5-u-float-right", - "title": "Host details", - "url": "/hosts/host-foo.example.com", - }, - ], - "associated_id": null, - "associated_name": null, - "associated_type": null, - "audit_title": "host-foo.example.com", - "audit_title_url": "/audits?search=type+%3D+host+and+auditable_id+%3D+9", - "auditable_id": 9, - "auditable_name": "host-foo.example.com", - "auditable_type": "Host::Base", - "audited_changes": Object { - "comment": Array [ - "", - "This is info about host for audit", - ], - "root_pass": Array [ - "[redacted]", - "[redacted]", - ], - }, - "audited_changes_with_id_to_label": Array [ - Object { - "change": Array [ - Object { - "css_class": "show-old", - "id_to_label": "[redacted]", - }, - Object { - "css_class": "show-new", - "id_to_label": "[redacted]", - }, - ], - "name": "Root pass", - }, - Object { - "change": Array [ - Object { - "css_class": "show-old", - "id_to_label": "[empty]", - }, - Object { - "css_class": "show-new", - "id_to_label": "This is info about host for audit", - }, - ], - "name": "Comment", - }, - ], - "audited_type_name": "Host", - "comment": null, - "created_at": "2018-08-13 00:34:55 -1100", - "id": 234, - "remote_address": "127.0.0.1", - "request_uuid": "c134239d-8ac3-494b-9962-35133fe153ba", - "user_id": 4, - "user_info": Object { - "audit_path": "/audits?search=id+%3D+234", - "display_name": "Admin ", - "login": "admin", - "search_path": "/audits?search=user+%3D+admin", - }, - "user_type": null, - "username": "Admin User", - "version": 2, - }, - ], - "hasData": true, - }, - "type": "AUDITS_PAGE_DATA_RESOLVED", - }, - ], -] -`; - -exports[`AuditsPage actions should fetch Audits and fail 1`] = ` -Array [ - Array [ - Object { - "type": "AUDITS_PAGE_SHOW_LOADING", - }, - ], - Array [ - Object { - "payload": Object { - "message": Object { - "text": "some-status some status text", - "type": "error", - }, - }, - "type": "AUDITS_PAGE_DATA_FAILED", - }, - ], -] -`; - -exports[`AuditsPage actions should fetch Audits and remove emptyState 1`] = ` -Array [ - Array [ - Object { - "type": "AUDITS_PAGE_SHOW_LOADING", - }, - ], - Array [ - Object { - "payload": Object { - "itemCount": 1, - "page": 1, - "perPage": 20, - "searchQuery": "", - }, - "type": "AUDITS_PAGE_UPDATE_QUERY", - }, - ], - Array [ - Object { - "payload": Object { - "audits": Array [ - Object { - "action": "update", - "action_display_name": "updated", - "affected_locations": Array [ - Object { - "name": "test_loc1", - "url": "/locations/2-test_loc1/edit", - }, - Object { - "name": "test_loc2", - "url": "/locations/6-test_loc2/edit", - }, - Object { - "name": "test_loc3", - "url": "/locations/9-test_loc3/edit", - }, - ], - "affected_organizations": Array [ - Object { - "name": "test_org1", - "url": "/organizations/1-test_org1/edit", - }, - Object { - "name": "test_org2", - "url": "/organizations/3-test_org2/edit", - }, - Object { - "name": "test_org3", - "url": "/organizations/5-test_org3/edit", - }, - ], - "allowed_actions": Array [ - Object { - "css_class": "pf-v5-c-button pf-m-primary pf-m-small pf-v5-u-float-right", - "title": "Host details", - "url": "/hosts/host-foo.example.com", - }, - ], - "associated_id": null, - "associated_name": null, - "associated_type": null, - "audit_title": "host-foo.example.com", - "audit_title_url": "/audits?search=type+%3D+host+and+auditable_id+%3D+9", - "auditable_id": 9, - "auditable_name": "host-foo.example.com", - "auditable_type": "Host::Base", - "audited_changes": Object { - "comment": Array [ - "", - "This is info about host for audit", - ], - "root_pass": Array [ - "[redacted]", - "[redacted]", - ], - }, - "audited_changes_with_id_to_label": Array [ - Object { - "change": Array [ - Object { - "css_class": "show-old", - "id_to_label": "[redacted]", - }, - Object { - "css_class": "show-new", - "id_to_label": "[redacted]", - }, - ], - "name": "Root pass", - }, - Object { - "change": Array [ - Object { - "css_class": "show-old", - "id_to_label": "[empty]", - }, - Object { - "css_class": "show-new", - "id_to_label": "This is info about host for audit", - }, - ], - "name": "Comment", - }, - ], - "audited_type_name": "Host", - "comment": null, - "created_at": "2018-08-13 00:34:55 -1100", - "id": 234, - "remote_address": "127.0.0.1", - "request_uuid": "c134239d-8ac3-494b-9962-35133fe153ba", - "user_id": 4, - "user_info": Object { - "audit_path": "/audits?search=id+%3D+234", - "display_name": "Admin ", - "login": "admin", - "search_path": "/audits?search=user+%3D+admin", - }, - "user_type": null, - "username": "Admin User", - "version": 2, - }, - ], - "hasData": true, - }, - "type": "AUDITS_PAGE_DATA_RESOLVED", - }, - ], -] -`; - -exports[`AuditsPage actions should fetch empty Audits 1`] = ` -Array [ - Array [ - Object { - "type": "AUDITS_PAGE_SHOW_LOADING", - }, - ], - Array [ - Object { - "payload": Object { - "itemCount": 0, - "page": 1, - "perPage": 20, - "searchQuery": "no-such-audit", - }, - "type": "AUDITS_PAGE_UPDATE_QUERY", - }, - ], - Array [ - Object { - "payload": Object { - "audits": Array [], - "hasData": false, - }, - "type": "AUDITS_PAGE_DATA_RESOLVED", - }, - ], -] -`; - -exports[`AuditsPage actions should fetchAndPush 1`] = ` -Array [ - Array [ - [Function], - ], -] -`; - -exports[`AuditsPage actions should initializeAudits 1`] = ` -Array [ - Array [ - [Function], - ], -] -`;