From c9ba9d443c17af03d47836b982267970bc66563f Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 23 Jun 2021 21:28:33 -0400 Subject: [PATCH] Add tests for all studio-member-actions --- src/views/studio/lib/studio-member-actions.js | 3 +- test/unit/redux/studio-member-actions.test.js | 377 ++++++++++++++++++ 2 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 test/unit/redux/studio-member-actions.test.js diff --git a/src/views/studio/lib/studio-member-actions.js b/src/views/studio/lib/studio-member-actions.js index c8eb56db0..36cd3e3ab 100644 --- a/src/views/studio/lib/studio-member-actions.js +++ b/src/views/studio/lib/studio-member-actions.js @@ -14,7 +14,8 @@ const Errors = keyMirror({ USER_MUTED: null, UNKNOWN_USERNAME: null, RATE_LIMIT: null, - MANAGER_LIMIT: null + MANAGER_LIMIT: null, + UNHANDLED: null }); const normalizeError = (err, body, res) => { diff --git a/test/unit/redux/studio-member-actions.test.js b/test/unit/redux/studio-member-actions.test.js new file mode 100644 index 000000000..1800aeb10 --- /dev/null +++ b/test/unit/redux/studio-member-actions.test.js @@ -0,0 +1,377 @@ +import {selectStudioManagerCount} from '../../../src/redux/studio'; +import { + Errors, + removeManager, + loadManagers, + loadCurators, + removeCurator, + inviteCurator, + promoteCurator, + acceptInvitation +} from '../../../src/views/studio/lib/studio-member-actions'; +import {managers, curators} from '../../../src/views/studio/lib/redux-modules'; +import {reducers, initialState} from '../../../src/views/studio/studio-redux'; +import configureStore from '../../../src/lib/configure-store'; + +jest.mock('../../../src/lib/api'); +import api from '../../../src/lib/api'; + +let store; + +beforeEach(() => { + api.mockClear(); +}); + +describe('loadManagers', () => { + test('it populates the managers list', () => { + store = configureStore(reducers, { + ...initialState, + studio: {id: 123123} + }); + api.mockImplementation((opts, callback) => { + const body = [{username: 'user1'}, {username: 'user2'}, {username: 'user3'}]; + callback(null, body, {statusCode: 200}); + }); + store.dispatch(loadManagers()); + let items = managers.selector(store.getState()).items; + expect(api.mock.calls[0][0].uri).toBe('/studios/123123/managers/'); + expect(api.mock.calls[0][0].params.offset).toBe(0); + expect(items.length).toBe(3); + expect(items[0].username).toBe('user1'); + + // Include the new offset next time it is called + store.dispatch(loadManagers()); + expect(api.mock.calls[1][0].params.offset).toBe(3); + items = managers.selector(store.getState()).items; + expect(items.length).toBe(6); + }); + + test('it correctly uses the admin route when possible', () => { + store = configureStore(reducers, { + ...initialState, + studio: {id: 123123}, + session: { + session: { + user: {token: 'a-token'}, + permissions: {admin: true} + } + } + }); + api.mockImplementation((opts, callback) => { + expect(opts.uri).toBe('/admin/studios/123123/managers/'); + expect(opts.authentication).toBe('a-token'); + }); + store.dispatch(loadManagers()); + }); + + test('errors are set on the managers state', () => { + store = configureStore(reducers, { + ...initialState, + studio: {id: 123123} + }); + api.mockImplementation((opts, callback) => { + callback(null, null, {statusCode: 500}); + }); + store.dispatch(loadManagers()); + expect(managers.selector(store.getState()).error).toBe(Errors.SERVER); + }); +}); + + +describe('loadCurators', () => { + test('it populates the curators list', () => { + store = configureStore(reducers, { + ...initialState, + studio: {id: 123123} + }); + api.mockImplementation((opts, callback) => { + const body = [{username: 'user1'}, {username: 'user2'}, {username: 'user3'}]; + callback(null, body, {statusCode: 200}); + }); + store.dispatch(loadCurators()); + let items = curators.selector(store.getState()).items; + expect(api.mock.calls[0][0].uri).toBe('/studios/123123/curators/'); + expect(api.mock.calls[0][0].params.offset).toBe(0); + expect(items.length).toBe(3); + expect(items[0].username).toBe('user1'); + + // Include the new offset next time it is called + store.dispatch(loadCurators()); + expect(api.mock.calls[1][0].params.offset).toBe(3); + items = curators.selector(store.getState()).items; + expect(items.length).toBe(6); + }); + + test('it correctly uses the admin route when possible', () => { + store = configureStore(reducers, { + ...initialState, + studio: {id: 123123}, + session: { + session: { + user: {token: 'a-token'}, + permissions: {admin: true} + } + } + }); + api.mockImplementation((opts, callback) => { + expect(opts.uri).toBe('/admin/studios/123123/curators/'); + expect(opts.authentication).toBe('a-token'); + }); + store.dispatch(loadCurators()); + }); + + test('errors are set on the curators state', () => { + store = configureStore(reducers, { + ...initialState, + studio: {id: 123123} + }); + api.mockImplementation((opts, callback) => { + callback(null, null, {statusCode: 500}); + }); + store.dispatch(loadCurators()); + expect(curators.selector(store.getState()).error).toBe(Errors.SERVER); + }); +}); + +describe('removeManager', () => { + beforeEach(() => { + store = configureStore(reducers, { + ...initialState, + studio: { + id: 123123, + managers: 3, + manager: true + }, + managers: { + items: [ + {username: 'user1'}, + {username: 'user2'}, + {username: 'user3'} + ] + }, + session: { + session: { + user: {username: 'user2'} + } + } + }); + }); + + test('removes the manager by username and decrements the count', async () => { + api.mockImplementation((opts, callback) => { + expect(opts.uri).toBe('/site-api/users/curators-in/123123/remove/'); + callback(null, {}, {statusCode: 200}); + }); + + await store.dispatch(removeManager('user2')); + const state = store.getState(); + + // Ensure it removes the correct manager (index=1) + expect(selectStudioManagerCount(state)).toBe(2); + expect(managers.selector(state).items[0].username).toBe('user1'); + expect(managers.selector(state).items[1].username).toBe('user3'); + + // Ensure roles change if you are removing yourself + expect(state.studio.manager).toBe(false); + }); + + test('on error, promise rejects without any changing count or list', async () => { + api.mockImplementation((opts, callback) => { + callback(null, {}, {statusCode: 403}); + }); + + await expect(store.dispatch(removeManager('user2'))) + .rejects.toBe(Errors.PERMISSION); + + const state = store.getState(); + const {items} = managers.selector(state); + expect(selectStudioManagerCount(state)).toBe(3); + expect(items.length).toBe(3); + }); +}); + +describe('removeCurator', () => { + beforeEach(() => { + store = configureStore(reducers, { + ...initialState, + studio: {id: 123123}, + curators: { + items: [ + {username: 'user1'}, + {username: 'user2'}, + {username: 'user3'} + ] + } + }); + }); + + test('removes the curator by username', async () => { + api.mockImplementation((opts, callback) => { + expect(opts.uri).toBe('/site-api/users/curators-in/123123/remove/'); + callback(null, {}, {statusCode: 200}); + }); + + await store.dispatch(removeCurator('user2')); + const state = store.getState(); + + // Ensure it removes the correct curator (index=1) + expect(curators.selector(state).items[0].username).toBe('user1'); + expect(curators.selector(state).items[1].username).toBe('user3'); + }); + + test('on error, promise rejects without changing anything', async () => { + api.mockImplementation((opts, callback) => { + callback(null, {}, {statusCode: 500}); + }); + + await expect(store.dispatch(removeCurator('user2'))) + .rejects.toBe(Errors.SERVER); + + const state = store.getState(); + const {items} = curators.selector(state); + expect(items.length).toBe(3); + }); +}); + +describe('inviteCurator', () => { + beforeEach(() => { + store = configureStore(reducers, { + ...initialState, + studio: {id: 123123} + }); + }); + + test('invites the curator on success', async () => { + api.mockImplementation((opts, callback) => { + callback(null, {}, {statusCode: 200}); + }); + + const result = await store.dispatch(inviteCurator('user2')); + expect(result).toBe('user2'); + expect(api.mock.calls[0][0].uri).toBe('/site-api/users/curators-in/123123/invite_curator/'); + expect(api.mock.calls[0][0].params.usernames).toBe('user2'); + }); + + test('error because of unknown user', async () => { + api.mockImplementation((opts, callback) => { + callback(null, {}, {statusCode: 404}); + }); + await expect(store.dispatch(inviteCurator('user2'))) + .rejects.toBe(Errors.UNKNOWN_USERNAME); + }); + test('error because of duplicate curator', async () => { + api.mockImplementation((opts, callback) => { + callback(null, {status: 'error', message: 'already a curator'}, {statusCode: 200}); + }); + await expect(store.dispatch(inviteCurator('user2'))) + .rejects.toBe(Errors.DUPLICATE); + }); + test('unhandled error response', async () => { + api.mockImplementation((opts, callback) => { + callback(null, {status: 'error', message: 'xyz'}, {statusCode: 200}); + }); + await expect(store.dispatch(inviteCurator('user2'))) + .rejects.toBe(Errors.UNHANDLED); + }); +}); + +describe('promoteCurator', () => { + beforeEach(() => { + store = configureStore(reducers, { + ...initialState, + studio: {id: 123123, managers: 0}, + curators: { + items: [{username: 'curatorName'}] + } + }); + }); + + test('promotes the curator on success', async () => { + api.mockImplementation((opts, callback) => { + callback(null, {}, {statusCode: 200}); + }); + + await store.dispatch(promoteCurator('curatorName')); + const state = store.getState(); + const {items: curatorList} = curators.selector(state); + const {items: managerList} = managers.selector(state); + + expect(api.mock.calls[0][0].uri).toBe('/site-api/users/curators-in/123123/promote/'); + expect(api.mock.calls[0][0].params.usernames).toBe('curatorName'); + expect(managerList.length).toBe(1); + expect(managerList[0].username).toBe('curatorName'); + expect(curatorList.length).toBe(0); + expect(selectStudioManagerCount(state)).toBe(1); + }); + + test('on error, promise rejects and nothing is modified', async () => { + api.mockImplementation((opts, callback) => { + callback(null, {}, {statusCode: 403}); + }); + await expect(store.dispatch(promoteCurator('curatorName'))) + .rejects.toBe(Errors.PERMISSION); + const state = store.getState(); + const {items: curatorList} = curators.selector(state); + const {items: managerList} = managers.selector(state); + expect(managerList.length).toBe(0); + expect(curatorList.length).toBe(1); + expect(selectStudioManagerCount(state)).toBe(0); + }); + + test('error because of exceeding manager limit', async () => { + api.mockImplementation((opts, callback) => { + callback(null, {message: 'too many owners'}, {statusCode: 400}); + }); + await expect(store.dispatch(promoteCurator('curatorName'))) + .rejects.toBe(Errors.MANAGER_LIMIT); + }); +}); + +describe('acceptInvitation', () => { + beforeEach(() => { + store = configureStore(reducers, { + ...initialState, + studio: {id: 123123, invited: true, curator: false}, + session: { + session: { + user: { + username: 'me' + } + } + } + }); + }); + + test('accepts the invitation on success', async () => { + api.mockImplementation((opts, callback) => { + callback(null, {username: 'me'}, {statusCode: 200}); + }); + jest.useFakeTimers(); + await store.dispatch(acceptInvitation()); + let state = store.getState(); + const {items: curatorList} = curators.selector(state); + expect(api.mock.calls[0][0].uri).toBe('/site-api/users/curators-in/123123/add/'); + expect(api.mock.calls[0][0].params.usernames).toBe('me'); + expect(curatorList.length).toBe(1); + expect(curatorList[0].username).toBe('me'); + expect(state.studio.invited).toBe(true); // Should remain true until timers run + jest.runAllTimers(); // delay to show success alert before toggling invited back to false + state = store.getState(); + expect(state.studio.invited).toBe(false); + expect(state.studio.curator).toBe(true); + jest.useRealTimers(); + }); + + test('on error, promise rejects and nothing is modified', async () => { + api.mockImplementation((opts, callback) => { + callback(null, {}, {statusCode: 403}); + }); + await expect(store.dispatch(acceptInvitation())) + .rejects.toBe(Errors.PERMISSION); + const state = store.getState(); + const {items: curatorList} = curators.selector(state); + expect(curatorList.length).toBe(0); + expect(state.studio.invited).toBe(true); + expect(state.studio.curator).toBe(false); + }); +});