mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-23 23:57:55 -05:00
492 lines
18 KiB
JavaScript
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');
|
|
});
|
|
});
|