scratch-www/test/unit/redux/studio-member-actions.test.js
2023-10-24 14:03:18 -07:00

492 lines
18 KiB
JavaScript

import {selectStudioManagerCount} from '../../../src/redux/studio';
import {
Errors,
removeManager,
loadManagers,
loadCurators,
removeCurator,
inviteCurator,
promoteCurator,
acceptInvitation,
transferHost
} 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);
// Reload the list
store.dispatch(loadManagers(true));
expect(api.mock.calls[2][0].params.offset).toBe(0);
items = managers.selector(store.getState()).items;
expect(items.length).toBe(3);
});
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 => {
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()).catch(() => {});
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 => {
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('removing a manager that hasnt been loaded yet still works', async () => {
// This covers an edge case the code allows where you can remove a manager
// even if that manager hasn't been loaded into the paginated managers state yet.
api.mockImplementation((opts, callback) => {
callback(null, {}, {statusCode: 200}); // Server says that manager was removed
});
await store.dispatch(removeManager('user4'));
const state = store.getState();
// Manager count should still be updated
expect(selectStudioManagerCount(state)).toBe(2);
// The removed manager isn't the current user, so manager permission should be unchanged
expect(state.studio.manager).toBe(true);
// No change to the manager items list
expect(managers.selector(state).items.length).toBe(3);
});
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('error because of rate limit', async () => {
api.mockImplementation((opts, callback) => {
callback(null, {}, {statusCode: 429});
});
await expect(store.dispatch(inviteCurator('user2')))
.rejects.toBe(Errors.RATE_LIMIT);
});
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);
});
});
describe('transferHost', () => {
beforeEach(() => {
store = configureStore(reducers, {
...initialState,
studio: {
id: 123123,
managers: 3,
host: 'oldHost'
}
});
});
test('transfers the host on success', async () => {
api.mockImplementation((opts, callback) => {
callback(null, {}, {statusCode: 200});
});
await store.dispatch(transferHost('password', 'newHostName', 'newHostId'));
const state = store.getState();
expect(api.mock.calls[0][0].uri).toBe('/studios/123123/transfer/newHostName');
expect(state.studio.host).toBe('newHostId');
});
test('error because of permissions issue', async () => {
api.mockImplementation((opts, callback) => {
callback(null, {}, {statusCode: 403});
});
await expect(store.dispatch(transferHost('password', 'newHostName', 'newHostId')))
.rejects.toBe(Errors.PERMISSION);
const state = store.getState();
expect(state.studio.host).toBe('oldHost');
});
test('error because of authorization issue', async () => {
api.mockImplementation((opts, callback) => {
callback(null, {}, {statusCode: 401});
});
await expect(store.dispatch(transferHost('password', 'newHostName', 'newHostId')))
.rejects.toBe(Errors.PERMISSION);
const state = store.getState();
expect(state.studio.host).toBe('oldHost');
});
test('error because of issue with new host', async () => {
api.mockImplementation((opts, callback) => {
callback(null, {}, {statusCode: 409});
});
await expect(store.dispatch(transferHost('password', 'newHostName', 'newHostId')))
.rejects.toBe(Errors.CANNOT_BE_HOST);
const state = store.getState();
expect(state.studio.host).toBe('oldHost');
});
test('error because of incorrect password', async () => {
api.mockImplementation((opts, callback) => {
callback(null, {status: 'error', message: 'password incorrect'}, {statusCode: 401});
});
await expect(store.dispatch(transferHost('password', 'newHostName', 'newHostId')))
.rejects.toBe(Errors.PASSWORD);
const state = store.getState();
expect(state.studio.host).toBe('oldHost');
});
test('error because of too many password attempts', async () => {
api.mockImplementation((opts, callback) => {
callback(null, {status: 'error', message: 'try again later'}, {statusCode: 429});
});
await expect(store.dispatch(transferHost('password', 'newHostName', 'newHostId')))
.rejects.toBe(Errors.PASSWORD_ATTEMPT_LIMIT);
const state = store.getState();
expect(state.studio.host).toBe('oldHost');
});
test('error because of rate limit', async () => {
api.mockImplementation((opts, callback) => {
callback(null, {}, {statusCode: 429});
});
await expect(store.dispatch(transferHost('password', 'newHostName', 'newHostId')))
.rejects.toBe(Errors.RATE_LIMIT);
const state = store.getState();
expect(state.studio.host).toBe('oldHost');
});
});