mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-53467 Unrevert changes that update current user object (#23928)
* Revert "Revert "[MM-52547] Include current user profile in every redux action (#23219)"" This reverts commit8f96888b1a
. * Revert "Revert "[MM-52546] webapp/channels : Update current user and status on WebSocket reconnect (#23071)"" This reverts commit69ee162a6e
. * MM-53647 Fix overwriting the current user with sanitized data
This commit is contained in:
parent
116728424c
commit
2672af30ea
@ -0,0 +1,94 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
// ***************************************************************
|
||||||
|
// - [#] indicates a test step (e.g. #. Go to a page)
|
||||||
|
// - [*] indicates an assertion (e.g. * Check the title)
|
||||||
|
// - Use element ID when selecting an element. Create one if none.
|
||||||
|
// ***************************************************************
|
||||||
|
|
||||||
|
// Group: @channels
|
||||||
|
|
||||||
|
describe('MM-53377 Regression tests', () => {
|
||||||
|
let testTeam;
|
||||||
|
let testUser;
|
||||||
|
let testUser2;
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.apiUpdateConfig({
|
||||||
|
PrivacySettings: {
|
||||||
|
ShowEmailAddress: false,
|
||||||
|
ShowFullName: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.apiInitSetup().then(({team, user, offTopicUrl}) => {
|
||||||
|
testTeam = team;
|
||||||
|
testUser = user;
|
||||||
|
|
||||||
|
cy.apiCreateUser().then((payload) => {
|
||||||
|
testUser2 = payload.user;
|
||||||
|
cy.apiAddUserToTeam(testTeam.id, payload.user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit(offTopicUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// # Login as testUser
|
||||||
|
cy.apiLogin(testUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should still have your email loaded after using the at-mention autocomplete', () => {
|
||||||
|
// * Ensure that this user is not an admin
|
||||||
|
cy.wrap(testUser).its('roles').should('equal', 'system_user');
|
||||||
|
|
||||||
|
// # Send a couple at mentions, quickly enough that the at mention autocomplete won't appear
|
||||||
|
cy.uiPostMessageQuickly(`@${testUser.username} @${testUser2.username}`);
|
||||||
|
|
||||||
|
// # Open the profile popover for the current user
|
||||||
|
cy.contains('.mention-link', `@${testUser.username}`).click();
|
||||||
|
|
||||||
|
// * Ensure that all fields are visible for the current user
|
||||||
|
cy.get('#user-profile-popover').within(() => {
|
||||||
|
cy.findByText(`@${testUser.username}`).should('exist');
|
||||||
|
cy.findByText(`${testUser.first_name} ${testUser.last_name}`).should('exist');
|
||||||
|
cy.findByText(testUser.email).should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
// # Click anywhere to close profile popover
|
||||||
|
cy.get('#channelHeaderInfo').click();
|
||||||
|
|
||||||
|
// # Open the profile popover for another user
|
||||||
|
cy.contains('.mention-link', `@${testUser2.username}`).click();
|
||||||
|
|
||||||
|
// * Ensure that only the username is visible for another user
|
||||||
|
cy.get('#user-profile-popover').within(() => {
|
||||||
|
cy.findByText(`@${testUser2.username}`).should('exist');
|
||||||
|
cy.findByText(`${testUser2.first_name} ${testUser2.last_name}`).should('not.exist');
|
||||||
|
cy.findByText(testUser2.email).should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
// # Start to type another at mention so that the autocomplete loads
|
||||||
|
cy.get('#post_textbox').type(`@${testUser.username}`);
|
||||||
|
|
||||||
|
// # Wait for the autocomplete to appear with the current user in it
|
||||||
|
cy.get('.suggestion-list').within(() => {
|
||||||
|
cy.findByText(`@${testUser.username}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// # Clear the post textbox to hide the autocomplete
|
||||||
|
cy.get('#post_textbox').clear();
|
||||||
|
|
||||||
|
// # Open the profile popover for the current user again
|
||||||
|
cy.contains('.mention-link', `@${testUser.username}`).click();
|
||||||
|
|
||||||
|
// * Ensure that all fields are still visible for the current user
|
||||||
|
cy.get('#user-profile-popover').within(() => {
|
||||||
|
cy.findByText(`@${testUser.username}`).should('exist');
|
||||||
|
cy.findByText(`${testUser.first_name} ${testUser.last_name}`).should('exist');
|
||||||
|
cy.findByText(testUser.email).should('exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -234,7 +234,7 @@ export function reconnect() {
|
|||||||
// we can request for getPosts again when socket is connected
|
// we can request for getPosts again when socket is connected
|
||||||
dispatch(getPosts(currentChannelId));
|
dispatch(getPosts(currentChannelId));
|
||||||
}
|
}
|
||||||
StatusActions.loadStatusesForChannelAndSidebar();
|
dispatch(StatusActions.loadStatusesForChannelAndSidebar());
|
||||||
|
|
||||||
const crtEnabled = isCollapsedThreadsEnabled(state);
|
const crtEnabled = isCollapsedThreadsEnabled(state);
|
||||||
dispatch(TeamActions.getMyTeamUnreads(crtEnabled, true));
|
dispatch(TeamActions.getMyTeamUnreads(crtEnabled, true));
|
||||||
|
@ -35,7 +35,6 @@ import {getServerVersion} from 'mattermost-redux/selectors/entities/general';
|
|||||||
import {getCurrentUserId, getUsers} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUserId, getUsers} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
|
||||||
import {removeUserFromList} from 'mattermost-redux/utils/user_utils';
|
|
||||||
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
|
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
|
||||||
import {General} from 'mattermost-redux/constants';
|
import {General} from 'mattermost-redux/constants';
|
||||||
|
|
||||||
@ -215,12 +214,10 @@ export function getFilteredUsersStats(options: GetFilteredUsersStatsOpts = {}, u
|
|||||||
|
|
||||||
export function getProfiles(page = 0, perPage: number = General.PROFILE_CHUNK_SIZE, options: any = {}): ActionFunc {
|
export function getProfiles(page = 0, perPage: number = General.PROFILE_CHUNK_SIZE, options: any = {}): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const {currentUserId} = getState().entities.users;
|
|
||||||
let profiles: UserProfile[];
|
let profiles: UserProfile[];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
profiles = await Client4.getProfiles(page, perPage, options);
|
profiles = await Client4.getProfiles(page, perPage, options);
|
||||||
removeUserFromList(currentUserId, profiles);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
@ -280,12 +277,10 @@ export function getMissingProfilesByUsernames(usernames: string[]): ActionFunc {
|
|||||||
|
|
||||||
export function getProfilesByIds(userIds: string[], options?: any): ActionFunc {
|
export function getProfilesByIds(userIds: string[], options?: any): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const {currentUserId} = getState().entities.users;
|
|
||||||
let profiles: UserProfile[];
|
let profiles: UserProfile[];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
profiles = await Client4.getProfilesByIds(userIds, options);
|
profiles = await Client4.getProfilesByIds(userIds, options);
|
||||||
removeUserFromList(currentUserId, profiles);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
@ -303,12 +298,10 @@ export function getProfilesByIds(userIds: string[], options?: any): ActionFunc {
|
|||||||
|
|
||||||
export function getProfilesByUsernames(usernames: string[]): ActionFunc {
|
export function getProfilesByUsernames(usernames: string[]): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const {currentUserId} = getState().entities.users;
|
|
||||||
let profiles;
|
let profiles;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
profiles = await Client4.getProfilesByUsernames(usernames);
|
profiles = await Client4.getProfilesByUsernames(usernames);
|
||||||
removeUserFromList(currentUserId, profiles);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
@ -326,7 +319,6 @@ export function getProfilesByUsernames(usernames: string[]): ActionFunc {
|
|||||||
|
|
||||||
export function getProfilesInTeam(teamId: string, page: number, perPage: number = General.PROFILE_CHUNK_SIZE, sort = '', options: any = {}): ActionFunc {
|
export function getProfilesInTeam(teamId: string, page: number, perPage: number = General.PROFILE_CHUNK_SIZE, sort = '', options: any = {}): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const {currentUserId} = getState().entities.users;
|
|
||||||
let profiles;
|
let profiles;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -345,7 +337,7 @@ export function getProfilesInTeam(teamId: string, page: number, perPage: number
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||||
data: removeUserFromList(currentUserId, [...profiles]),
|
data: profiles,
|
||||||
},
|
},
|
||||||
]));
|
]));
|
||||||
|
|
||||||
@ -415,7 +407,6 @@ export enum ProfilesInChannelSortBy {
|
|||||||
|
|
||||||
export function getProfilesInChannel(channelId: string, page: number, perPage: number = General.PROFILE_CHUNK_SIZE, sort = '', options: {active?: boolean} = {}): ActionFunc {
|
export function getProfilesInChannel(channelId: string, page: number, perPage: number = General.PROFILE_CHUNK_SIZE, sort = '', options: {active?: boolean} = {}): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const {currentUserId} = getState().entities.users;
|
|
||||||
let profiles;
|
let profiles;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -434,7 +425,7 @@ export function getProfilesInChannel(channelId: string, page: number, perPage: n
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||||
data: removeUserFromList(currentUserId, [...profiles]),
|
data: profiles,
|
||||||
},
|
},
|
||||||
]));
|
]));
|
||||||
|
|
||||||
@ -444,7 +435,6 @@ export function getProfilesInChannel(channelId: string, page: number, perPage: n
|
|||||||
|
|
||||||
export function getProfilesInGroupChannels(channelsIds: string[]): ActionFunc {
|
export function getProfilesInGroupChannels(channelsIds: string[]): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const {currentUserId} = getState().entities.users;
|
|
||||||
let channelProfiles;
|
let channelProfiles;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -468,7 +458,7 @@ export function getProfilesInGroupChannels(channelsIds: string[]): ActionFunc {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||||
data: removeUserFromList(currentUserId, [...profiles]),
|
data: profiles,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -482,7 +472,6 @@ export function getProfilesInGroupChannels(channelsIds: string[]): ActionFunc {
|
|||||||
|
|
||||||
export function getProfilesNotInChannel(teamId: string, channelId: string, groupConstrained: boolean, page: number, perPage: number = General.PROFILE_CHUNK_SIZE): ActionFunc {
|
export function getProfilesNotInChannel(teamId: string, channelId: string, groupConstrained: boolean, page: number, perPage: number = General.PROFILE_CHUNK_SIZE): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const {currentUserId} = getState().entities.users;
|
|
||||||
let profiles;
|
let profiles;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -503,7 +492,7 @@ export function getProfilesNotInChannel(teamId: string, channelId: string, group
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||||
data: removeUserFromList(currentUserId, [...profiles]),
|
data: profiles,
|
||||||
},
|
},
|
||||||
]));
|
]));
|
||||||
|
|
||||||
@ -564,7 +553,6 @@ export function updateMyTermsOfServiceStatus(termsOfServiceId: string, accepted:
|
|||||||
|
|
||||||
export function getProfilesInGroup(groupId: string, page = 0, perPage: number = General.PROFILE_CHUNK_SIZE, sort = ''): ActionFunc {
|
export function getProfilesInGroup(groupId: string, page = 0, perPage: number = General.PROFILE_CHUNK_SIZE, sort = ''): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const {currentUserId} = getState().entities.users;
|
|
||||||
let profiles;
|
let profiles;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -583,7 +571,7 @@ export function getProfilesInGroup(groupId: string, page = 0, perPage: number =
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||||
data: removeUserFromList(currentUserId, [...profiles]),
|
data: profiles,
|
||||||
},
|
},
|
||||||
]));
|
]));
|
||||||
|
|
||||||
@ -593,7 +581,6 @@ export function getProfilesInGroup(groupId: string, page = 0, perPage: number =
|
|||||||
|
|
||||||
export function getProfilesNotInGroup(groupId: string, page = 0, perPage: number = General.PROFILE_CHUNK_SIZE): ActionFunc {
|
export function getProfilesNotInGroup(groupId: string, page = 0, perPage: number = General.PROFILE_CHUNK_SIZE): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const {currentUserId} = getState().entities.users;
|
|
||||||
let profiles;
|
let profiles;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -612,7 +599,7 @@ export function getProfilesNotInGroup(groupId: string, page = 0, perPage: number
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||||
data: removeUserFromList(currentUserId, [...profiles]),
|
data: profiles,
|
||||||
},
|
},
|
||||||
]));
|
]));
|
||||||
|
|
||||||
@ -844,9 +831,6 @@ export function autocompleteUsers(term: string, teamId = '', channelId = '', opt
|
|||||||
}): ActionFunc {
|
}): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
dispatch({type: UserTypes.AUTOCOMPLETE_USERS_REQUEST, data: null});
|
dispatch({type: UserTypes.AUTOCOMPLETE_USERS_REQUEST, data: null});
|
||||||
|
|
||||||
const {currentUserId} = getState().entities.users;
|
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await Client4.autocompleteUsers(term, teamId, channelId, options);
|
data = await Client4.autocompleteUsers(term, teamId, channelId, options);
|
||||||
@ -861,7 +845,6 @@ export function autocompleteUsers(term: string, teamId = '', channelId = '', opt
|
|||||||
if (data.out_of_channel) {
|
if (data.out_of_channel) {
|
||||||
users = [...users, ...data.out_of_channel];
|
users = [...users, ...data.out_of_channel];
|
||||||
}
|
}
|
||||||
removeUserFromList(currentUserId, users);
|
|
||||||
const actions: AnyAction[] = [{
|
const actions: AnyAction[] = [{
|
||||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||||
data: users,
|
data: users,
|
||||||
@ -904,8 +887,6 @@ export function autocompleteUsers(term: string, teamId = '', channelId = '', opt
|
|||||||
|
|
||||||
export function searchProfiles(term: string, options: any = {}): ActionFunc {
|
export function searchProfiles(term: string, options: any = {}): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const {currentUserId} = getState().entities.users;
|
|
||||||
|
|
||||||
let profiles;
|
let profiles;
|
||||||
try {
|
try {
|
||||||
profiles = await Client4.searchUsers(term, options);
|
profiles = await Client4.searchUsers(term, options);
|
||||||
@ -915,7 +896,7 @@ export function searchProfiles(term: string, options: any = {}): ActionFunc {
|
|||||||
return {error};
|
return {error};
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions: AnyAction[] = [{type: UserTypes.RECEIVED_PROFILES_LIST, data: removeUserFromList(currentUserId, [...profiles])}];
|
const actions: AnyAction[] = [{type: UserTypes.RECEIVED_PROFILES_LIST, data: profiles}];
|
||||||
|
|
||||||
if (options.in_channel_id) {
|
if (options.in_channel_id) {
|
||||||
actions.push({
|
actions.push({
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {UserProfile} from '@mattermost/types/users';
|
||||||
|
import {IDMappedObjects} from '@mattermost/types/utilities';
|
||||||
|
|
||||||
import {UserTypes, ChannelTypes} from 'mattermost-redux/action_types';
|
import {UserTypes, ChannelTypes} from 'mattermost-redux/action_types';
|
||||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||||
import reducer from 'mattermost-redux/reducers/entities/users';
|
import reducer from 'mattermost-redux/reducers/entities/users';
|
||||||
|
import deepFreezeAndThrowOnMutation from 'mattermost-redux/utils/deep_freeze';
|
||||||
|
|
||||||
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
type ReducerState = ReturnType<typeof reducer>;
|
type ReducerState = ReturnType<typeof reducer>;
|
||||||
|
|
||||||
describe('Reducers.users', () => {
|
describe('Reducers.users', () => {
|
||||||
@ -673,4 +680,331 @@ describe('Reducers.users', () => {
|
|||||||
expect(newState.profilesNotInGroup).toEqual(expectedState.profilesNotInGroup);
|
expect(newState.profilesNotInGroup).toEqual(expectedState.profilesNotInGroup);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('profiles', () => {
|
||||||
|
function sanitizeUser(user: UserProfile) {
|
||||||
|
const sanitized = {
|
||||||
|
...user,
|
||||||
|
email: '',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
auth_service: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
Reflect.deleteProperty(sanitized, 'email_verify');
|
||||||
|
Reflect.deleteProperty(sanitized, 'last_password_update');
|
||||||
|
Reflect.deleteProperty(sanitized, 'notify_props');
|
||||||
|
Reflect.deleteProperty(sanitized, 'terms_of_service_id');
|
||||||
|
Reflect.deleteProperty(sanitized, 'terms_of_service_create_at');
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const actionType of [UserTypes.RECEIVED_ME, UserTypes.RECEIVED_PROFILE]) {
|
||||||
|
test(`should store a new user (${actionType})`, () => {
|
||||||
|
const user1 = TestHelper.getUserMock({id: 'user_id1'});
|
||||||
|
const user2 = TestHelper.getUserMock({id: 'user_id2'});
|
||||||
|
|
||||||
|
const state = deepFreezeAndThrowOnMutation({
|
||||||
|
profiles: {
|
||||||
|
[user1.id]: user1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextState = reducer(state, {
|
||||||
|
type: actionType,
|
||||||
|
data: user2,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState.profiles).toEqual({
|
||||||
|
[user1.id]: user1,
|
||||||
|
[user2.id]: user2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`should update an existing user (${actionType})`, () => {
|
||||||
|
const user1 = TestHelper.getUserMock({id: 'user_id1'});
|
||||||
|
|
||||||
|
const state = deepFreezeAndThrowOnMutation({
|
||||||
|
profiles: {
|
||||||
|
[user1.id]: user1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextState = reducer(state, {
|
||||||
|
type: actionType,
|
||||||
|
data: {
|
||||||
|
...user1,
|
||||||
|
username: 'a different username',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState.profiles).toEqual({
|
||||||
|
[user1.id]: {
|
||||||
|
...user1,
|
||||||
|
username: 'a different username',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`should not overwrite unsanitized data with sanitized data (${actionType})`, () => {
|
||||||
|
const user1 = TestHelper.getUserMock({
|
||||||
|
id: 'user_id1',
|
||||||
|
email: 'user1@example.com',
|
||||||
|
first_name: 'User',
|
||||||
|
last_name: 'One',
|
||||||
|
auth_service: 'saml',
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = deepFreezeAndThrowOnMutation({
|
||||||
|
profiles: {
|
||||||
|
[user1.id]: user1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextState = reducer(state, {
|
||||||
|
type: actionType,
|
||||||
|
data: {
|
||||||
|
...sanitizeUser(user1),
|
||||||
|
username: 'a different username',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState.profiles).toEqual({
|
||||||
|
[user1.id]: {
|
||||||
|
...user1,
|
||||||
|
username: 'a different username',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(nextState.profiles[user1.id].email).toBe(user1.email);
|
||||||
|
expect(nextState.profiles[user1.id].auth_service).toBe(user1.auth_service);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`should return the same state when given an identical user object (${actionType})`, () => {
|
||||||
|
const user1 = TestHelper.getUserMock({id: 'user_id1'});
|
||||||
|
|
||||||
|
const state = deepFreezeAndThrowOnMutation({
|
||||||
|
profiles: {
|
||||||
|
[user1.id]: user1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextState = reducer(state, {
|
||||||
|
type: actionType,
|
||||||
|
data: user1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState.profiles).toBe(state.profiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`should return the same state when given an sanitized but otherwise identical user object (${actionType})`, () => {
|
||||||
|
const user1 = TestHelper.getUserMock({
|
||||||
|
id: 'user_id1',
|
||||||
|
email: 'user1@example.com',
|
||||||
|
first_name: 'User',
|
||||||
|
last_name: 'One',
|
||||||
|
auth_service: 'saml',
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = deepFreezeAndThrowOnMutation({
|
||||||
|
profiles: {
|
||||||
|
[user1.id]: user1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextState = reducer(state, {
|
||||||
|
type: actionType,
|
||||||
|
data: sanitizeUser(user1),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState.profiles).toBe(state.profiles);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const actionType of [UserTypes.RECEIVED_PROFILES, UserTypes.RECEIVED_PROFILES_LIST]) {
|
||||||
|
function usersToData(users: UserProfile[]) {
|
||||||
|
if (actionType === UserTypes.RECEIVED_PROFILES) {
|
||||||
|
const userMap: IDMappedObjects<UserProfile> = {};
|
||||||
|
for (const user of users) {
|
||||||
|
userMap[user.id] = user;
|
||||||
|
}
|
||||||
|
return userMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
test(`should store new users (${actionType})`, () => {
|
||||||
|
const user1 = TestHelper.getUserMock({id: 'user_id1'});
|
||||||
|
const user2 = TestHelper.getUserMock({id: 'user_id2'});
|
||||||
|
const user3 = TestHelper.getUserMock({id: 'user_id3'});
|
||||||
|
|
||||||
|
const state = deepFreezeAndThrowOnMutation({
|
||||||
|
profiles: {
|
||||||
|
[user1.id]: user1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextState = reducer(state, {
|
||||||
|
type: actionType,
|
||||||
|
data: usersToData([user2, user3]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState.profiles).toEqual({
|
||||||
|
[user1.id]: user1,
|
||||||
|
[user2.id]: user2,
|
||||||
|
[user3.id]: user3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`should update existing users (${actionType})`, () => {
|
||||||
|
const user1 = TestHelper.getUserMock({id: 'user_id1'});
|
||||||
|
const user2 = TestHelper.getUserMock({id: 'user_id2'});
|
||||||
|
const user3 = TestHelper.getUserMock({id: 'user_id3'});
|
||||||
|
|
||||||
|
const state = deepFreezeAndThrowOnMutation({
|
||||||
|
profiles: {
|
||||||
|
[user1.id]: user1,
|
||||||
|
[user2.id]: user2,
|
||||||
|
[user3.id]: user3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const newUser1 = {
|
||||||
|
...user1,
|
||||||
|
username: 'a different username',
|
||||||
|
};
|
||||||
|
const newUser2 = {
|
||||||
|
...user2,
|
||||||
|
nickname: 'a different nickname',
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextState = reducer(state, {
|
||||||
|
type: actionType,
|
||||||
|
data: usersToData([newUser1, newUser2]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState.profiles).toEqual({
|
||||||
|
[user1.id]: newUser1,
|
||||||
|
[user2.id]: newUser2,
|
||||||
|
[user3.id]: user3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`should not overwrite unsanitized data with sanitized data (${actionType})`, () => {
|
||||||
|
const user1 = TestHelper.getUserMock({
|
||||||
|
id: 'user_id1',
|
||||||
|
email: 'user1@example.com',
|
||||||
|
first_name: 'User',
|
||||||
|
last_name: 'One',
|
||||||
|
auth_service: 'saml',
|
||||||
|
});
|
||||||
|
const user2 = TestHelper.getUserMock({id: 'user_id2'});
|
||||||
|
|
||||||
|
const state = deepFreezeAndThrowOnMutation({
|
||||||
|
profiles: {
|
||||||
|
[user1.id]: user1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const newUser1 = {
|
||||||
|
...sanitizeUser(user1),
|
||||||
|
username: 'a different username',
|
||||||
|
};
|
||||||
|
const newUser2 = {
|
||||||
|
...sanitizeUser(user2),
|
||||||
|
nickname: 'a different nickname',
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextState = reducer(state, {
|
||||||
|
type: actionType,
|
||||||
|
data: usersToData([newUser1, newUser2]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState.profiles).toEqual({
|
||||||
|
[user1.id]: {
|
||||||
|
...user1,
|
||||||
|
username: 'a different username',
|
||||||
|
},
|
||||||
|
[user2.id]: newUser2,
|
||||||
|
});
|
||||||
|
expect(nextState.profiles[user1.id].email).toBe(user1.email);
|
||||||
|
expect(nextState.profiles[user1.id].auth_service).toBe(user1.auth_service);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`should return the same state when given identical user objects (${actionType})`, () => {
|
||||||
|
const user1 = TestHelper.getUserMock({id: 'user_id1'});
|
||||||
|
const user2 = TestHelper.getUserMock({id: 'user_id2'});
|
||||||
|
|
||||||
|
const state = deepFreezeAndThrowOnMutation({
|
||||||
|
profiles: {
|
||||||
|
[user1.id]: user1,
|
||||||
|
[user2.id]: user2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextState = reducer(state, {
|
||||||
|
type: actionType,
|
||||||
|
data: usersToData([user1, user2]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState.profiles).toBe(state.profiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`should return the same state when given an sanitized but otherwise identical user object (${actionType})`, () => {
|
||||||
|
const user1 = TestHelper.getUserMock({
|
||||||
|
id: 'user_id1',
|
||||||
|
email: 'user1@example.com',
|
||||||
|
first_name: 'User',
|
||||||
|
last_name: 'One',
|
||||||
|
auth_service: 'saml',
|
||||||
|
});
|
||||||
|
const user2 = TestHelper.getUserMock({id: 'user_id2'});
|
||||||
|
|
||||||
|
const state = deepFreezeAndThrowOnMutation({
|
||||||
|
profiles: {
|
||||||
|
[user1.id]: user1,
|
||||||
|
[user2.id]: user2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextState = reducer(state, {
|
||||||
|
type: actionType,
|
||||||
|
data: usersToData([sanitizeUser(user1), sanitizeUser(user2)]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nextState.profiles).toBe(state.profiles);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('UserTypes.RECEIVED_PROFILES_LIST, should merge existing users with new ones', () => {
|
||||||
|
const firstUser = TestHelper.getUserMock({id: 'first_user_id'});
|
||||||
|
const secondUser = TestHelper.getUserMock({id: 'seocnd_user_id'});
|
||||||
|
const thirdUser = TestHelper.getUserMock({id: 'third_user_id'});
|
||||||
|
const partialUpdatedFirstUser = {
|
||||||
|
...firstUser,
|
||||||
|
update_at: 123456789,
|
||||||
|
};
|
||||||
|
Reflect.deleteProperty(partialUpdatedFirstUser, 'email');
|
||||||
|
Reflect.deleteProperty(partialUpdatedFirstUser, 'notify_props');
|
||||||
|
const state = {
|
||||||
|
profiles: {
|
||||||
|
first_user_id: firstUser,
|
||||||
|
second_user_id: secondUser,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const action = {
|
||||||
|
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||||
|
data: [
|
||||||
|
partialUpdatedFirstUser,
|
||||||
|
thirdUser,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const {profiles: newProfiles} = reducer(state as unknown as ReducerState, action);
|
||||||
|
|
||||||
|
expect(newProfiles.first_user_id).toEqual({...firstUser, ...partialUpdatedFirstUser});
|
||||||
|
expect(newProfiles.second_user_id).toEqual(secondUser);
|
||||||
|
expect(newProfiles.third_user_id).toEqual(thirdUser);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -105,7 +105,7 @@ function removeProfileFromSet(state: RelationOneToMany<Team, UserProfile>, actio
|
|||||||
function currentUserId(state = '', action: GenericAction) {
|
function currentUserId(state = '', action: GenericAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case UserTypes.RECEIVED_ME: {
|
case UserTypes.RECEIVED_ME: {
|
||||||
const data = action.data || action.payload;
|
const data = action.data;
|
||||||
|
|
||||||
return data.id;
|
return data.id;
|
||||||
}
|
}
|
||||||
@ -173,62 +173,81 @@ function myAudits(state = [], action: GenericAction) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function profiles(state: IDMappedObjects<UserProfile> = {}, action: GenericAction) {
|
function receiveUserProfile(state: IDMappedObjects<UserProfile>, received: UserProfile) {
|
||||||
switch (action.type) {
|
const existing = state[received.id];
|
||||||
case UserTypes.RECEIVED_ME:
|
|
||||||
case UserTypes.RECEIVED_PROFILE: {
|
|
||||||
const data = action.data || action.payload;
|
|
||||||
const user = {...data};
|
|
||||||
const oldUser = state[data.id];
|
|
||||||
if (oldUser) {
|
|
||||||
user.terms_of_service_id = oldUser.terms_of_service_id;
|
|
||||||
user.terms_of_service_create_at = oldUser.terms_of_service_create_at;
|
|
||||||
|
|
||||||
if (isEqual(user, oldUser)) {
|
if (!existing) {
|
||||||
return state;
|
// No existing data to merge with
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[received.id]: received,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const merged = {
|
||||||
|
...existing,
|
||||||
|
...received,
|
||||||
|
};
|
||||||
|
|
||||||
|
// MM-53377:
|
||||||
|
// For non-admin users, certain API responses don't return details for the current user that would be sanitized
|
||||||
|
// out for others. This currently includes:
|
||||||
|
// - email (if PrivacySettings.ShowEmailAddress is false)
|
||||||
|
// - first_name/last_name (if PrivacySettings.ShowFullName is false)
|
||||||
|
// - last_password_update
|
||||||
|
// - auth_service
|
||||||
|
// - notify_props
|
||||||
|
//
|
||||||
|
// Because email, first_name, last_name, and auth_service can all be empty strings regularly, we can't just
|
||||||
|
// merge the received user and the existing one together like we normally would. Instead, we can use the
|
||||||
|
// existence of existing.notify_props or existing.last_password_update to determine which object has that extra
|
||||||
|
// data so that it can take precedence. Those fields are:
|
||||||
|
// 1. Never empty or zero by Go standards
|
||||||
|
// 2. Only ever sent to the current user, not even to admins, so we know that the object contains privileged data
|
||||||
|
//
|
||||||
|
// Note that admins may have the email/name/auth_service of other users loaded as well. This does not prevent that
|
||||||
|
// data from being replaced when merging sanitized user objects. There doesn't seem to be a way for us to detect
|
||||||
|
// whether the object is sanitized for admins.
|
||||||
|
if (existing.notify_props && (!received.notify_props || Object.keys(received.notify_props).length === 0)) {
|
||||||
|
merged.email = existing.email;
|
||||||
|
merged.first_name = existing.first_name;
|
||||||
|
merged.last_name = existing.last_name;
|
||||||
|
merged.last_password_update = existing.last_password_update;
|
||||||
|
merged.auth_service = existing.auth_service;
|
||||||
|
merged.notify_props = existing.notify_props;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEqual(existing, merged)) {
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[data.id]: user,
|
[merged.id]: merged,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function profiles(state: IDMappedObjects<UserProfile> = {}, action: GenericAction) {
|
||||||
|
switch (action.type) {
|
||||||
|
case UserTypes.RECEIVED_ME:
|
||||||
|
case UserTypes.RECEIVED_PROFILE: {
|
||||||
|
const user = action.data;
|
||||||
|
|
||||||
|
return receiveUserProfile(state, user);
|
||||||
}
|
}
|
||||||
case UserTypes.RECEIVED_PROFILES_LIST: {
|
case UserTypes.RECEIVED_PROFILES_LIST: {
|
||||||
const users: UserProfile[] = action.data;
|
const users: UserProfile[] = action.data;
|
||||||
|
|
||||||
return users.reduce((nextState, user) => {
|
return users.reduce(receiveUserProfile, state);
|
||||||
const oldUser = nextState[user.id];
|
|
||||||
|
|
||||||
if (oldUser && isEqual(user, oldUser)) {
|
|
||||||
return nextState;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...nextState,
|
|
||||||
[user.id]: user,
|
|
||||||
};
|
|
||||||
}, state);
|
|
||||||
}
|
}
|
||||||
case UserTypes.RECEIVED_PROFILES: {
|
case UserTypes.RECEIVED_PROFILES: {
|
||||||
const users: UserProfile[] = Object.values(action.data);
|
const users: UserProfile[] = Object.values(action.data);
|
||||||
|
|
||||||
return users.reduce((nextState, user) => {
|
return users.reduce(receiveUserProfile, state);
|
||||||
const oldUser = nextState[user.id];
|
|
||||||
|
|
||||||
if (oldUser && isEqual(user, oldUser)) {
|
|
||||||
return nextState;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...nextState,
|
|
||||||
[user.id]: user,
|
|
||||||
};
|
|
||||||
}, state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case UserTypes.RECEIVED_TERMS_OF_SERVICE_STATUS: {
|
case UserTypes.RECEIVED_TERMS_OF_SERVICE_STATUS: {
|
||||||
const data = action.data || action.payload;
|
const data = action.data;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[data.user_id]: {
|
[data.user_id]: {
|
||||||
|
Loading…
Reference in New Issue
Block a user