diff --git a/lib/permissions/minimally-encoded-thread-permissions-test-data.js b/lib/permissions/minimally-encoded-thread-permissions-test-data.js index 2ee60f702..4f356f1cb 100644 --- a/lib/permissions/minimally-encoded-thread-permissions-test-data.js +++ b/lib/permissions/minimally-encoded-thread-permissions-test-data.js @@ -1,726 +1,724 @@ // @flow import type { RawThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { threadTypes } from '../types/thread-types-enum.js'; import type { LegacyRawThreadInfo } from '../types/thread-types.js'; const exampleRawThreadInfoA: LegacyRawThreadInfo = { id: '85171', type: threadTypes.GENESIS_PERSONAL, name: '', description: '', color: '6d49ab', creationTime: 1675887298557, parentThreadID: '1', members: [ { id: '256', role: null, permissions: { know_of: { value: true, source: '1', }, visible: { value: true, source: '1', }, voiced: { value: true, source: '1', }, edit_entries: { value: true, source: '1', }, edit_thread: { value: true, source: '1', }, edit_thread_description: { value: true, source: '1', }, edit_thread_color: { value: true, source: '1', }, delete_thread: { value: true, source: '1', }, create_subthreads: { value: true, source: '1', }, create_sidebars: { value: true, source: '1', }, join_thread: { value: true, source: '1', }, edit_permissions: { value: true, source: '1', }, add_members: { value: true, source: '1', }, remove_members: { value: true, source: '1', }, change_role: { value: true, source: '1', }, leave_thread: { value: false, source: null, }, react_to_message: { value: false, source: null, }, edit_message: { value: false, source: null, }, manage_pins: { value: true, source: '1', }, voiced_in_announcement_channels: { value: false, source: null, }, manage_farcaster_channel_tags: { value: false, source: null, }, }, isSender: false, }, { id: '83853', role: '85172', permissions: { know_of: { value: true, source: '85171', }, visible: { value: true, source: '85171', }, voiced: { value: true, source: '85171', }, edit_entries: { value: true, source: '85171', }, edit_thread: { value: true, source: '85171', }, edit_thread_description: { value: true, source: '85171', }, edit_thread_color: { value: true, source: '85171', }, delete_thread: { value: false, source: null, }, create_subthreads: { value: false, source: null, }, create_sidebars: { value: true, source: '85171', }, join_thread: { value: false, source: null, }, edit_permissions: { value: false, source: null, }, add_members: { value: false, source: null, }, remove_members: { value: false, source: null, }, change_role: { value: false, source: null, }, leave_thread: { value: false, source: null, }, react_to_message: { value: true, source: '85171', }, edit_message: { value: true, source: '85171', }, manage_pins: { value: false, source: null, }, voiced_in_announcement_channels: { value: false, source: null, }, manage_farcaster_channel_tags: { value: false, source: null, }, }, isSender: true, }, ], roles: { '85172': { id: '85172', name: 'Members', permissions: { know_of: true, visible: true, voiced: true, react_to_message: true, edit_message: true, edit_entries: true, edit_thread: true, edit_thread_color: true, edit_thread_description: true, create_sidebars: true, descendant_open_know_of: true, descendant_open_visible: true, child_open_join_thread: true, }, isDefault: true, }, }, currentUser: { role: '85172', permissions: { know_of: { value: true, source: '85171', }, visible: { value: true, source: '85171', }, voiced: { value: true, source: '85171', }, edit_entries: { value: true, source: '85171', }, edit_thread: { value: true, source: '85171', }, edit_thread_description: { value: true, source: '85171', }, edit_thread_color: { value: true, source: '85171', }, delete_thread: { value: false, source: null, }, create_subthreads: { value: false, source: null, }, create_sidebars: { value: true, source: '85171', }, join_thread: { value: false, source: null, }, edit_permissions: { value: false, source: null, }, add_members: { value: false, source: null, }, remove_members: { value: false, source: null, }, change_role: { value: false, source: null, }, leave_thread: { value: false, source: null, }, react_to_message: { value: true, source: '85171', }, edit_message: { value: true, source: '85171', }, manage_pins: { value: false, source: null, }, voiced_in_announcement_channels: { value: false, source: null, }, manage_farcaster_channel_tags: { value: false, source: null, }, }, subscription: { home: true, pushNotifs: true, }, unread: false, }, repliesCount: 0, containingThreadID: '1', community: '1', pinnedCount: 0, }; const exampleMinimallyEncodedRawThreadInfoA: RawThreadInfo = { minimallyEncoded: true, id: '85171', type: threadTypes.GENESIS_PERSONAL, name: '', description: '', color: '6d49ab', creationTime: 1675887298557, parentThreadID: '1', members: [ { minimallyEncoded: true, id: '256', role: null, - permissions: '87fff', isSender: false, }, { minimallyEncoded: true, id: '83853', role: '85172', - permissions: '3027f', isSender: true, }, ], roles: { '85172': { minimallyEncoded: true, id: '85172', name: 'Members', permissions: [ '000', '010', '020', '100', '110', '030', '040', '060', '050', '090', '005', '015', '0a9', ], }, }, currentUser: { minimallyEncoded: true, role: '85172', permissions: '3027f', subscription: { home: true, pushNotifs: true, }, unread: false, }, repliesCount: 0, containingThreadID: '1', community: '1', pinnedCount: 0, }; const expectedDecodedExampleRawThreadInfoA: LegacyRawThreadInfo = { id: '85171', type: threadTypes.GENESIS_PERSONAL, name: '', description: '', color: '6d49ab', creationTime: 1675887298557, parentThreadID: '1', members: [ { id: '256', role: null, permissions: { know_of: { value: true, source: 'null', }, visible: { value: true, source: 'null', }, voiced: { value: true, source: 'null', }, edit_entries: { value: true, source: 'null', }, edit_thread: { value: true, source: 'null', }, edit_thread_description: { value: true, source: 'null', }, edit_thread_color: { value: true, source: 'null', }, delete_thread: { value: true, source: 'null', }, create_subthreads: { value: true, source: 'null', }, create_sidebars: { value: true, source: 'null', }, join_thread: { value: true, source: 'null', }, edit_permissions: { value: true, source: 'null', }, add_members: { value: true, source: 'null', }, remove_members: { value: true, source: 'null', }, change_role: { value: true, source: 'null', }, leave_thread: { value: false, source: null, }, react_to_message: { value: false, source: null, }, edit_message: { value: false, source: null, }, manage_pins: { value: true, source: 'null', }, manage_invite_links: { source: null, value: false, }, edit_thread_avatar: { source: null, value: false, }, voiced_in_announcement_channels: { value: false, source: null, }, manage_farcaster_channel_tags: { value: false, source: null, }, }, isSender: false, }, { id: '83853', role: '85172', permissions: { know_of: { value: true, source: 'null', }, visible: { value: true, source: 'null', }, voiced: { value: true, source: 'null', }, edit_entries: { value: true, source: 'null', }, edit_thread: { value: true, source: 'null', }, edit_thread_description: { value: true, source: 'null', }, edit_thread_color: { value: true, source: 'null', }, delete_thread: { value: false, source: null, }, create_subthreads: { value: false, source: null, }, create_sidebars: { value: true, source: 'null', }, join_thread: { value: false, source: null, }, edit_permissions: { value: false, source: null, }, add_members: { value: false, source: null, }, remove_members: { value: false, source: null, }, change_role: { value: false, source: null, }, leave_thread: { value: false, source: null, }, react_to_message: { value: true, source: 'null', }, edit_message: { value: true, source: 'null', }, manage_pins: { value: false, source: null, }, manage_invite_links: { source: null, value: false, }, edit_thread_avatar: { source: null, value: false, }, voiced_in_announcement_channels: { value: false, source: null, }, manage_farcaster_channel_tags: { value: false, source: null, }, }, isSender: true, }, ], roles: { '85172': { id: '85172', name: 'Members', permissions: { know_of: true, visible: true, voiced: true, react_to_message: true, edit_message: true, edit_entries: true, edit_thread: true, edit_thread_color: true, edit_thread_description: true, create_sidebars: true, descendant_open_know_of: true, descendant_open_visible: true, child_open_join_thread: true, }, isDefault: true, }, }, currentUser: { role: '85172', permissions: { know_of: { value: true, source: 'null', }, visible: { value: true, source: 'null', }, voiced: { value: true, source: 'null', }, edit_entries: { value: true, source: 'null', }, edit_thread: { value: true, source: 'null', }, edit_thread_description: { value: true, source: 'null', }, edit_thread_color: { value: true, source: 'null', }, delete_thread: { value: false, source: null, }, create_subthreads: { value: false, source: null, }, create_sidebars: { value: true, source: 'null', }, join_thread: { value: false, source: null, }, edit_permissions: { value: false, source: null, }, add_members: { value: false, source: null, }, remove_members: { value: false, source: null, }, change_role: { value: false, source: null, }, leave_thread: { value: false, source: null, }, react_to_message: { value: true, source: 'null', }, edit_message: { value: true, source: 'null', }, manage_pins: { value: false, source: null, }, manage_invite_links: { source: null, value: false, }, edit_thread_avatar: { source: null, value: false, }, voiced_in_announcement_channels: { value: false, source: null, }, manage_farcaster_channel_tags: { value: false, source: null, }, }, subscription: { home: true, pushNotifs: true, }, unread: false, }, repliesCount: 0, containingThreadID: '1', community: '1', pinnedCount: 0, }; export { exampleRawThreadInfoA, exampleMinimallyEncodedRawThreadInfoA, expectedDecodedExampleRawThreadInfoA, }; diff --git a/lib/permissions/special-roles.test.js b/lib/permissions/special-roles.test.js index 622d7d324..ec6a7ead1 100644 --- a/lib/permissions/special-roles.test.js +++ b/lib/permissions/special-roles.test.js @@ -1,223 +1,219 @@ // @flow import { patchRawThreadInfosWithSpecialRole, patchRoleInfoWithSpecialRole, specialRoles, } from './special-roles.js'; import type { RoleInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { threadTypes } from '../types/thread-types-enum.js'; import type { RawThreadInfos } from '../types/thread-types.js'; describe('patchRoleInfoWithSpecialRole', () => { it('should correctly set DEFAULT_ROLE', () => { const role: RoleInfo = { minimallyEncoded: true, id: 'roleID', name: 'roleName', permissions: ['abc', 'def'], specialRole: specialRoles.DEFAULT_ROLE, }; const patchedRole = patchRoleInfoWithSpecialRole(role); expect(patchedRole.specialRole).toBe(specialRoles.DEFAULT_ROLE); }); it('should correctly set ADMIN_ROLE', () => { const role: RoleInfo = { minimallyEncoded: true, id: 'roleID', name: 'Admins', permissions: ['abc', 'def'], }; const patchedRole = patchRoleInfoWithSpecialRole(role); expect(patchedRole.specialRole).toBe(specialRoles.ADMIN_ROLE); }); it('should correctly set undefined', () => { const role: RoleInfo = { minimallyEncoded: true, id: 'roleID', name: 'BLAH', permissions: ['abc', 'def'], }; const patchedRole = patchRoleInfoWithSpecialRole(role); expect(patchedRole.specialRole).toBe(null); }); }); const rawThreadInfos: RawThreadInfos = { '256|1': { minimallyEncoded: true, id: '256|1', type: threadTypes.GENESIS, name: 'GENESIS', description: 'This is the first community on Comm. In the future it will be possible to create chats outside of a community, but for now all of these chats get set with GENESIS as their parent. GENESIS is hosted on Ashoat’s keyserver.', color: 'c85000', creationTime: 1702415956354, parentThreadID: null, containingThreadID: null, community: null, members: [ { id: '256', role: '256|83796', - permissions: '3f73ff', isSender: false, minimallyEncoded: true, }, { id: '83809', role: '256|83795', - permissions: '3', isSender: false, minimallyEncoded: true, }, ], roles: { '256|83795': { id: '256|83795', name: 'Members', permissions: ['000', '010', '005', '015', '0a7'], minimallyEncoded: true, specialRole: specialRoles.DEFAULT_ROLE, }, '256|83796': { id: '256|83796', name: 'Admins', permissions: [ '000', '010', '020', '100', '110', '030', '040', '060', '050', '120', '080', '090', '0c0', '070', '0d0', '0e0', '130', '140', '150', '004', '014', '0a6', '0a8', '024', '034', '044', '064', '054', '124', '086', '096', '0c4', '074', '0b4', '0d4', '0e4', '134', '156', ], minimallyEncoded: true, }, }, currentUser: { role: '256|83795', permissions: '3', subscription: { home: true, pushNotifs: true, }, unread: false, minimallyEncoded: true, }, repliesCount: 0, pinnedCount: 0, }, '256|83814': { id: '256|83814', type: threadTypes.GENESIS_PRIVATE, name: '', description: 'This is your private chat, where you can set reminders and jot notes in private!', color: 'aa4b4b', creationTime: 1702415964471, parentThreadID: '256|1', repliesCount: 0, containingThreadID: '256|1', community: '256|1', pinnedCount: 0, minimallyEncoded: true, members: [ { id: '256', role: null, - permissions: '2c7fff', isSender: false, minimallyEncoded: true, }, { id: '83809', role: '256|83815', - permissions: '3026f', isSender: true, minimallyEncoded: true, }, ], roles: { '256|83815': { id: '256|83815', name: 'NotMembers', permissions: [ '000', '010', '020', '100', '110', '060', '050', '090', '030', '005', '015', '0a9', ], minimallyEncoded: true, }, }, currentUser: { role: '256|83815', permissions: '3026f', subscription: { home: true, pushNotifs: true, }, unread: false, minimallyEncoded: true, }, }, }; describe('patchRawThreadInfosWithSpecialRole', () => { it('should correctly set special roles', () => { const patchedRawThreadInfos = patchRawThreadInfosWithSpecialRole(rawThreadInfos); expect(patchedRawThreadInfos['256|1'].roles['256|83795'].specialRole).toBe( specialRoles.DEFAULT_ROLE, ); expect(patchedRawThreadInfos['256|1'].roles['256|83796'].specialRole).toBe( specialRoles.ADMIN_ROLE, ); expect( patchedRawThreadInfos['256|83814'].roles['256|83815'].specialRole, ).toBe(null); }); }); diff --git a/lib/reducers/calendar-filters-reducer.test.js b/lib/reducers/calendar-filters-reducer.test.js index ade99a219..f8286ade8 100644 --- a/lib/reducers/calendar-filters-reducer.test.js +++ b/lib/reducers/calendar-filters-reducer.test.js @@ -1,219 +1,215 @@ // @flow import reduceCalendarFilters, { removeDeletedThreadIDsFromFilterList, removeKeyserverThreadIDsFromFilterList, } from './calendar-filters-reducer.js'; import { keyserverAuthActionTypes } from '../actions/user-actions.js'; import type { RawMessageInfo } from '../types/message-types.js'; import type { ThreadStore } from '../types/thread-types'; const calendarFilters = [ { type: 'threads', threadIDs: ['256|1', '256|83815'] }, ]; const threadStore: ThreadStore = { threadInfos: { '256|1': { id: '256|1', type: 12, name: 'GENESIS', description: '', color: '648caa', creationTime: 1689091732528, parentThreadID: null, repliesCount: 0, containingThreadID: null, community: null, pinnedCount: 0, minimallyEncoded: true, members: [ { id: '256', role: '256|83796', - permissions: '3f73ff', isSender: true, minimallyEncoded: true, }, { id: '83810', role: '256|83795', - permissions: '3', isSender: false, minimallyEncoded: true, }, ], roles: { '256|83795': { id: '256|83795', name: 'Members', permissions: ['000', '010', '005', '015', '0a7'], minimallyEncoded: true, }, '256|83796': { id: '256|83796', name: 'Admins', permissions: ['000', '010', '005', '015', '0a7'], minimallyEncoded: true, }, }, currentUser: { role: '256|83795', permissions: '3', subscription: { home: true, pushNotifs: true, }, unread: false, minimallyEncoded: true, }, }, '256|83815': { id: '256|83815', type: 7, name: '', description: 'This is your private chat, where you can set reminders and jot notes in private!', color: '57697f', creationTime: 1689248242797, parentThreadID: '256|1', repliesCount: 0, containingThreadID: '256|1', community: '256|1', pinnedCount: 0, minimallyEncoded: true, members: [ { id: '256', role: null, - permissions: '2c7fff', isSender: false, minimallyEncoded: true, }, { id: '83810', role: '256|83816', - permissions: '3026f', isSender: true, minimallyEncoded: true, }, ], roles: { '256|83816': { id: '256|83816', name: 'Members', permissions: ['000', '010', '005', '015', '0a7'], minimallyEncoded: true, }, }, currentUser: { role: null, permissions: '3026f', subscription: { home: true, pushNotifs: true, }, unread: false, minimallyEncoded: true, }, }, }, }; describe('removeDeletedThreadIDsFromFilterList', () => { it('Removes threads the user is not a member of anymore', () => { expect( removeDeletedThreadIDsFromFilterList( calendarFilters, threadStore.threadInfos, ), ).toEqual([{ type: 'threads', threadIDs: ['256|1'] }]); }); }); const threadIDsToStay = [ '256|1', '256|2', '200|4', '300|5', '300|6', '256|100', ]; const keyserverToRemove1 = '100'; const keyserverToRemove2 = '400'; const threadIDsToRemove = [ keyserverToRemove1 + '|3', keyserverToRemove1 + '|7', keyserverToRemove2 + '|8', ]; const calendarFiltersState = [ { type: 'not_deleted' }, { type: 'threads', threadIDs: [...threadIDsToStay, ...threadIDsToRemove], }, ]; describe('removeKeyserverThreadIDsFromFilterList', () => { it('Removes threads from the given keyserver', () => { expect( removeKeyserverThreadIDsFromFilterList(calendarFiltersState, [ keyserverToRemove1, keyserverToRemove2, ]), ).toEqual([ { type: 'not_deleted' }, { type: 'threads', threadIDs: threadIDsToStay, }, ]); }); }); describe('reduceCalendarFilters', () => { it('Removes from filters thread ids of the keyservers that were logged into', () => { const messageInfos: RawMessageInfo[] = []; const payload = { currentUserInfo: { id: '5', username: 'me' }, preRequestUserInfo: { id: '5', username: 'me' }, threadInfos: {}, messagesResult: { currentAsOf: {}, messageInfos, truncationStatus: {}, watchedIDsAtRequestTime: [], }, userInfos: [], calendarResult: { rawEntryInfos: [], calendarQuery: { startDate: '0', endDate: '0', filters: [] }, }, // only updatesCurrentAsOf is relevant to this test updatesCurrentAsOf: { [keyserverToRemove1]: 5, [keyserverToRemove2]: 5 }, authActionSource: 'SOCKET_AUTH_ERROR_RESOLUTION_ATTEMPT', }; expect( reduceCalendarFilters( calendarFiltersState, { type: keyserverAuthActionTypes.success, payload, loadingInfo: { customKeyName: null, trackMultipleRequests: false, fetchIndex: 1, }, }, { threadInfos: {} }, ), ).toEqual([ { type: 'not_deleted' }, { type: 'threads', threadIDs: threadIDsToStay, }, ]); }); }); diff --git a/lib/reducers/integrity-reducer.test.js b/lib/reducers/integrity-reducer.test.js index 4239710f6..cc995ec5b 100644 --- a/lib/reducers/integrity-reducer.test.js +++ b/lib/reducers/integrity-reducer.test.js @@ -1,230 +1,228 @@ // @flow import { reduceIntegrityStore } from './integrity-reducer.js'; import { updateIntegrityStoreActionType } from '../actions/integrity-actions.js'; import type { ThreadStoreOperation } from '../ops/thread-store-ops'; import { type ThreadHashes } from '../types/integrity-types.js'; import type { RawThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { hash } from '../utils/objects.js'; jest.mock('../utils/config.js'); const currentThreadHashes: ThreadHashes = { '256|2204191': 1029852, '256|2205980': 3119392, '256|2208693': 4157082, '256|2212631': 8951764, }; const threadIDToUpdate = '256|2210486'; const threadIDsToUpdateList = [threadIDToUpdate]; type ThreadInfos = { +[string]: RawThreadInfo, }; const threadInfos: ThreadInfos = { [threadIDToUpdate]: { currentUser: { role: '256|83795', permissions: '3', minimallyEncoded: true, unread: true, subscription: { pushNotifs: true, home: true, }, }, id: threadIDToUpdate, type: 12, name: 'GENESIS', description: '', color: '648caa', creationTime: 1689091732528, parentThreadID: null, repliesCount: 0, containingThreadID: null, community: null, pinnedCount: 0, minimallyEncoded: true, members: [ { id: '256', role: '256|83796', - permissions: '3f73ff', isSender: true, minimallyEncoded: true, }, { id: '83810', role: '256|83795', - permissions: '3', isSender: false, minimallyEncoded: true, }, ], roles: { '256|83795': { id: '256|83795', name: 'Members', permissions: ['000', '010', '005', '015', '0a7'], minimallyEncoded: true, }, '256|83796': { id: '256|83796', name: 'Admins', permissions: ['000', '010', '005', '015', '0a7'], minimallyEncoded: true, }, }, }, }; const threadHashToUpdate = hash(threadInfos[threadIDToUpdate]); describe('reduceIntegrityStore', () => { it('should update integrity store with new thread hash', () => { const oldIntegrityStore = { threadHashes: currentThreadHashes, threadHashingStatus: 'completed', }; const updateThreadHashesAction = { type: updateIntegrityStoreActionType, payload: { threadIDsToHash: threadIDsToUpdateList }, }; expect( reduceIntegrityStore( oldIntegrityStore, updateThreadHashesAction, () => null, threadInfos, [], ).integrityStore, ).toEqual({ threadHashes: { ...currentThreadHashes, [threadIDToUpdate]: threadHashToUpdate, }, threadHashingStatus: 'completed', }); }); it('should update integrity store with new thread hash', () => { const oldIntegrityStore = { threadHashes: currentThreadHashes, threadHashingStatus: 'completed', }; const updateThreadHashesAction = { type: updateIntegrityStoreActionType, payload: { threadHashingStatus: 'completed' }, }; const threadStoreOperations: Array = [ { type: 'replace', payload: { id: threadIDToUpdate, threadInfo: threadInfos[threadIDToUpdate], }, }, ]; expect( reduceIntegrityStore( oldIntegrityStore, updateThreadHashesAction, () => null, threadInfos, threadStoreOperations, ).integrityStore, ).toEqual({ threadHashes: { ...currentThreadHashes, [threadIDToUpdate]: threadHashToUpdate, }, threadHashingStatus: 'completed', }); }); it('should remove two thread hashes', () => { const oldIntegrityStore = { threadHashes: currentThreadHashes, threadHashingStatus: 'completed', }; const thread_ids_remove = ['256|2204191', '256|2205980']; const thread_hashes_after_removal: ThreadHashes = { '256|2208693': 4157082, '256|2212631': 8951764, }; const updateThreadHashesAction = { type: updateIntegrityStoreActionType, payload: { threadHashingStatus: 'completed' }, }; const threadStoreOperations: Array = [ { type: 'remove', payload: { ids: thread_ids_remove, }, }, ]; expect( reduceIntegrityStore( oldIntegrityStore, updateThreadHashesAction, () => null, threadInfos, threadStoreOperations, ).integrityStore, ).toEqual({ threadHashes: thread_hashes_after_removal, threadHashingStatus: 'completed', }); }); it('should clear thread hashes and update with a single thread hash', () => { const oldIntegrityStore = { threadHashes: currentThreadHashes, threadHashingStatus: 'completed', }; const updateThreadHashesAction = { type: updateIntegrityStoreActionType, payload: { threadHashingStatus: 'completed' }, }; const threadStoreOperations: Array = [ { type: 'remove_all', }, { type: 'replace', payload: { id: threadIDToUpdate, threadInfo: threadInfos[threadIDToUpdate], }, }, ]; expect( reduceIntegrityStore( oldIntegrityStore, updateThreadHashesAction, () => null, threadInfos, threadStoreOperations, ).integrityStore, ).toEqual({ threadHashes: { [threadIDToUpdate]: threadHashToUpdate }, threadHashingStatus: 'completed', }); }); }); diff --git a/lib/shared/thread-utils.test.js b/lib/shared/thread-utils.test.js index e1aae0c54..860f852b9 100644 --- a/lib/shared/thread-utils.test.js +++ b/lib/shared/thread-utils.test.js @@ -1,199 +1,197 @@ // @flow import { parsePendingThreadID, threadInfoFromRawThreadInfo, } from './thread-utils.js'; import { threadInfoValidator } from '../permissions/minimally-encoded-thread-permissions-validators.js'; import type { RawThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { threadTypes } from '../types/thread-types-enum.js'; import type { UserInfos } from '../types/user-types.js'; describe('parsePendingThreadID(pendingThreadID: string)', () => { it('should return correct data for real pending sidebar ID', () => { const sidebarResult = { threadType: threadTypes.SIDEBAR, memberIDs: [], sourceMessageID: '12345', }; expect(parsePendingThreadID('pending/sidebar/12345')).toStrictEqual( sidebarResult, ); const sidebarResultWithNewSchema = { threadType: threadTypes.SIDEBAR, memberIDs: [], sourceMessageID: '789|12345', }; expect(parsePendingThreadID('pending/sidebar/789|12345')).toStrictEqual( sidebarResultWithNewSchema, ); }); it('should return correct data for real pending sidebar ID', () => { const pendingPersonalResult = { threadType: threadTypes.GENESIS_PERSONAL, memberIDs: ['83810', '86622'], sourceMessageID: null, }; expect(parsePendingThreadID('pending/type6/83810+86622')).toStrictEqual( pendingPersonalResult, ); const pendingCommunityOpenResult = { threadType: threadTypes.COMMUNITY_OPEN_SUBTHREAD, memberIDs: ['83810', '86622', '83889'], sourceMessageID: null, }; expect( parsePendingThreadID('pending/type3/83810+86622+83889'), ).toStrictEqual(pendingCommunityOpenResult); }); it('should return null when there are missing information in ID', () => { expect(parsePendingThreadID('pending/type4/')).toBeNull(); expect(parsePendingThreadID('type12/83810+86622')).toBeNull(); expect(parsePendingThreadID('pending/83810')).toBeNull(); expect(parsePendingThreadID('pending')).toBeNull(); expect(parsePendingThreadID('')).toBeNull(); expect(parsePendingThreadID('pending/something/12345')).toBeNull(); }); it('should return null when the format is invalid', () => { expect(parsePendingThreadID('someothertext/type1/12345')).toBeNull(); expect(parsePendingThreadID('pending/type6/12312+++11+12')).toBeNull(); expect(parsePendingThreadID('pending/type3/83810+')).toBeNull(); }); it('should throw invariant violation when thread type is invalid ', () => { expect(() => parsePendingThreadID('pending/type123/12345')).toThrowError( 'number is not ThreadType enum', ); }); }); const rawThreadInfo: RawThreadInfo = { id: '1', type: threadTypes.GENESIS, name: 'GENESIS', description: 'This is the first community on Comm. In the future it will be possible to create chats outside of a community, but for now all of these chats get set with GENESIS as their parent. GENESIS is hosted on Ashoat’s keyserver.', color: 'c85000', creationTime: 1702415956354, parentThreadID: null, repliesCount: 0, containingThreadID: null, community: null, pinnedCount: 0, minimallyEncoded: true, members: [ { id: '256', role: '83796', - permissions: '3f73ff', isSender: false, minimallyEncoded: true, }, { id: '83809', role: '83795', - permissions: '3', isSender: false, minimallyEncoded: true, }, ], roles: { '83795': { id: '83795', name: 'Members', permissions: ['000', '010', '005', '015', '0a7'], minimallyEncoded: true, }, '83796': { id: '83796', name: 'Admins', permissions: [ '000', '010', '020', '100', '110', '030', '040', '060', '050', '120', '080', '090', '0c0', '070', '0d0', '0e0', '130', '140', '150', '004', '014', '0a6', '0a8', '024', '034', '044', '064', '054', '124', '086', '096', '0c4', '074', '0b4', '0d4', '0e4', '134', '156', ], minimallyEncoded: true, }, }, currentUser: { role: '83795', permissions: '3', subscription: { home: true, pushNotifs: true, }, unread: false, minimallyEncoded: true, }, }; const userInfos: UserInfos = { '5': { id: '5', username: 'commbot', }, '256': { id: '256', username: 'ashoat', }, '83809': { id: '83809', username: 'atul', avatar: { type: 'emoji', emoji: '😲', color: '4b87aa', }, }, }; describe('threadInfoFromRawThreadInfo', () => { it('should return correctly formed ThreadInfo from RawThreadInfo', () => { const threadInfo = threadInfoFromRawThreadInfo( rawThreadInfo, null, userInfos, ); expect(threadInfoValidator.is(threadInfo)).toBe(true); }); }); diff --git a/lib/types/minimally-encoded-thread-permissions-types.js b/lib/types/minimally-encoded-thread-permissions-types.js index 8c0dd5335..09359816b 100644 --- a/lib/types/minimally-encoded-thread-permissions-types.js +++ b/lib/types/minimally-encoded-thread-permissions-types.js @@ -1,294 +1,294 @@ // @flow import invariant from 'invariant'; import _mapValues from 'lodash/fp/mapValues.js'; import type { ClientAvatar } from './avatar-types.js'; import type { ThreadPermissionsInfo } from './thread-permission-types.js'; import type { ThreadType } from './thread-types-enum.js'; import type { ClientLegacyRoleInfo, LegacyMemberInfo, LegacyRawThreadInfo, LegacyThickRawThreadInfo, LegacyThinRawThreadInfo, LegacyThreadCurrentUserInfo, ThickMemberInfo, } from './thread-types.js'; import { decodeThreadRolePermissionsBitmaskArray, permissionsToBitmaskHex, threadPermissionsFromBitmaskHex, threadRolePermissionsBlobToBitmaskArray, } from '../permissions/minimally-encoded-thread-permissions.js'; import type { SpecialRole } from '../permissions/special-roles.js'; import { specialRoles } from '../permissions/special-roles.js'; import { roleIsAdminRole, roleIsDefaultRole } from '../shared/thread-utils.js'; import type { ThreadEntity } from '../utils/entity-text.js'; type RoleInfoBase = $ReadOnly<{ +id: string, +name: string, +minimallyEncoded: true, +permissions: $ReadOnlyArray, }>; export type RoleInfo = $ReadOnly<{ ...RoleInfoBase, +specialRole?: ?SpecialRole, }>; const minimallyEncodeRoleInfo = (roleInfo: ClientLegacyRoleInfo): RoleInfo => { invariant( !('minimallyEncoded' in roleInfo), 'roleInfo is already minimally encoded.', ); let specialRole: ?SpecialRole; if (roleIsDefaultRole(roleInfo)) { specialRole = specialRoles.DEFAULT_ROLE; } else if (roleIsAdminRole(roleInfo)) { specialRole = specialRoles.ADMIN_ROLE; } const { isDefault, ...rest } = roleInfo; return { ...rest, minimallyEncoded: true, permissions: threadRolePermissionsBlobToBitmaskArray(roleInfo.permissions), specialRole, }; }; const decodeMinimallyEncodedRoleInfo = ( minimallyEncodedRoleInfo: RoleInfo, ): ClientLegacyRoleInfo => { const { minimallyEncoded, specialRole, ...rest } = minimallyEncodedRoleInfo; return { ...rest, permissions: decodeThreadRolePermissionsBitmaskArray( minimallyEncodedRoleInfo.permissions, ), isDefault: roleIsDefaultRole(minimallyEncodedRoleInfo), }; }; export type ThreadCurrentUserInfo = $ReadOnly<{ ...LegacyThreadCurrentUserInfo, +minimallyEncoded: true, +permissions: string, }>; const minimallyEncodeThreadCurrentUserInfo = ( threadCurrentUserInfo: LegacyThreadCurrentUserInfo, ): ThreadCurrentUserInfo => { invariant( !('minimallyEncoded' in threadCurrentUserInfo), 'threadCurrentUserInfo is already minimally encoded.', ); return { ...threadCurrentUserInfo, minimallyEncoded: true, permissions: permissionsToBitmaskHex(threadCurrentUserInfo.permissions), }; }; const decodeMinimallyEncodedThreadCurrentUserInfo = ( minimallyEncodedThreadCurrentUserInfo: ThreadCurrentUserInfo, ): LegacyThreadCurrentUserInfo => { const { minimallyEncoded, ...rest } = minimallyEncodedThreadCurrentUserInfo; return { ...rest, permissions: threadPermissionsFromBitmaskHex( minimallyEncodedThreadCurrentUserInfo.permissions, ), }; }; export type MemberInfoWithPermissions = $ReadOnly<{ ...LegacyMemberInfo, +minimallyEncoded: true, +permissions: string, }>; export type MemberInfoSansPermissions = $Diff< MemberInfoWithPermissions, { +permissions: string }, >; export type MinimallyEncodedThickMemberInfo = $ReadOnly<{ ...ThickMemberInfo, +minimallyEncoded: true, +permissions: string, }>; const minimallyEncodeMemberInfo = ( memberInfo: T, ): $ReadOnly<{ ...T, +minimallyEncoded: true, +permissions: string, }> => { invariant( !('minimallyEncoded' in memberInfo), 'memberInfo is already minimally encoded.', ); return { ...memberInfo, minimallyEncoded: true, permissions: permissionsToBitmaskHex(memberInfo.permissions), }; }; const decodeMinimallyEncodedMemberInfo = < T: MemberInfoWithPermissions | MinimallyEncodedThickMemberInfo, >( minimallyEncodedMemberInfo: T, ): $ReadOnly<{ ...$Diff< T, { +minimallyEncoded: true, +permissions: string, }, >, +permissions: ThreadPermissionsInfo, }> => { const { minimallyEncoded, ...rest } = minimallyEncodedMemberInfo; return { ...rest, permissions: threadPermissionsFromBitmaskHex( minimallyEncodedMemberInfo.permissions, ), }; }; export type ThinRawThreadInfo = $ReadOnly<{ ...LegacyThinRawThreadInfo, +minimallyEncoded: true, - +members: $ReadOnlyArray, + +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +currentUser: ThreadCurrentUserInfo, }>; export type ThickRawThreadInfo = $ReadOnly<{ ...LegacyThickRawThreadInfo, +minimallyEncoded: true, +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +currentUser: ThreadCurrentUserInfo, }>; export type RawThreadInfo = ThinRawThreadInfo | ThickRawThreadInfo; const deprecatedMinimallyEncodeRawThreadInfo = ( rawThreadInfo: LegacyRawThreadInfo, ): RawThreadInfo => { invariant( !('minimallyEncoded' in rawThreadInfo), 'rawThreadInfo is already minimally encoded.', ); if (rawThreadInfo.thick) { const { members, roles, currentUser, ...rest } = rawThreadInfo; return { ...rest, minimallyEncoded: true, members: members.map(minimallyEncodeMemberInfo), roles: _mapValues(minimallyEncodeRoleInfo)(roles), currentUser: minimallyEncodeThreadCurrentUserInfo(currentUser), }; } else { const { members, roles, currentUser, ...rest } = rawThreadInfo; // We removed the `.permissions` field from `MemberInfo`, but persisted // `MemberInfo`s will still have the field in legacy migrations. // $FlowIgnore return { ...rest, minimallyEncoded: true, members: members.map(minimallyEncodeMemberInfo), roles: _mapValues(minimallyEncodeRoleInfo)(roles), currentUser: minimallyEncodeThreadCurrentUserInfo(currentUser), }; } }; const deprecatedDecodeMinimallyEncodedRawThreadInfo = ( minimallyEncodedRawThreadInfo: RawThreadInfo, ): LegacyRawThreadInfo => { if (minimallyEncodedRawThreadInfo.thick) { const { minimallyEncoded, members, roles, currentUser, ...rest } = minimallyEncodedRawThreadInfo; return { ...rest, members: members.map(decodeMinimallyEncodedMemberInfo), roles: _mapValues(decodeMinimallyEncodedRoleInfo)(roles), currentUser: decodeMinimallyEncodedThreadCurrentUserInfo(currentUser), }; } else { const { minimallyEncoded, members, roles, currentUser, ...rest } = minimallyEncodedRawThreadInfo; return { ...rest, // We removed the `.permissions` field from `MemberInfo`, but persisted // `MemberInfo`s will still have the field in legacy migrations. // $FlowIgnore members: members.map(decodeMinimallyEncodedMemberInfo), roles: _mapValues(decodeMinimallyEncodedRoleInfo)(roles), currentUser: decodeMinimallyEncodedThreadCurrentUserInfo(currentUser), }; } }; export type RoleInfoWithoutSpecialRole = $ReadOnly<{ ...RoleInfoBase, +isDefault?: boolean, }>; export type RawThreadInfoWithoutSpecialRole = $ReadOnly<{ ...RawThreadInfo, +roles: { +[id: string]: RoleInfoWithoutSpecialRole }, }>; export type RelativeMemberInfo = { +id: string, +role: ?string, +isSender: boolean, +minimallyEncoded: true, +username: ?string, +isViewer: boolean, }; export type ThreadInfo = $ReadOnly<{ +minimallyEncoded: true, +id: string, +type: ThreadType, +name: ?string, +uiName: string | ThreadEntity, +avatar?: ?ClientAvatar, +description: ?string, +color: string, // hex, without "#" or "0x" +creationTime: number, // millisecond timestamp +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, +pinnedCount?: number, }>; export type ResolvedThreadInfo = $ReadOnly<{ ...ThreadInfo, +uiName: string, }>; export { minimallyEncodeRoleInfo, decodeMinimallyEncodedRoleInfo, minimallyEncodeThreadCurrentUserInfo, decodeMinimallyEncodedThreadCurrentUserInfo, minimallyEncodeMemberInfo, decodeMinimallyEncodedMemberInfo, deprecatedMinimallyEncodeRawThreadInfo, deprecatedDecodeMinimallyEncodedRawThreadInfo, };