diff --git a/.eslintrc.json b/.eslintrc.json index bde36073e..176949b3d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,80 +1,81 @@ { "root": true, "env": { - "es6": true + "es6": true, + "es2020": true }, "extends": [ "eslint:recommended", "plugin:react/recommended", "plugin:flowtype/recommended", "plugin:import/errors", "plugin:import/warnings", "plugin:prettier/recommended", "prettier" ], "parser": "babel-eslint", "plugins": ["react", "react-hooks", "flowtype", "monorepo", "import"], "rules": { // Prettier is configured to keep lines to 80 chars, but there are two issues: // - It doesn't handle comments (leaves them as-is) // - It makes all import statements take one line (reformats them) // We want ESLint to warn us in the first case, but not in the second case // since Prettier forces us in the second case. By setting code to 5000, we // make sure ESLint defers to Prettier for import statements. "max-len": ["error", { "code": 5000, "comments": 80, "ignoreUrls": true }], "flowtype/require-valid-file-annotation": ["error", "always"], "flowtype/require-exact-type": ["error", "never"], "curly": "error", "linebreak-style": "error", "semi": "error", "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", "monorepo/no-relative-import": "error", "no-empty": ["error", { "allowEmptyCatch": true }], "import/no-unresolved": 0, "no-unused-vars": ["error", { "ignoreRestSiblings": true }], "react/prop-types": ["error", { "skipUndeclared": true }], "no-shadow": 1, "no-var": "error", "import/extensions": ["error", "always"], "import/order": [ "warn", { "newlines-between": "always", "alphabetize": { "order": "asc", "caseInsensitive": true }, "groups": [["builtin", "external"], "internal"] } ], "prefer-const": "error", "react/jsx-curly-brace-presence": [ "error", { "props": "never", "children": "ignore" } ], "eqeqeq": ["error", "always"], "no-constant-condition": ["error", { "checkLoops": false }], "consistent-return": "error" }, "settings": { "react": { "version": "detect" }, "import/ignore": ["react-native"], "import/internal-regex": "^(lib|native|keyserver|web)/" }, "overrides": [ { "files": "*.cjs", "env": { "node": true, "commonjs": true }, "rules": { "flowtype/require-valid-file-annotation": 0, "flowtype/require-exact-type": 0 } } ] } diff --git a/keyserver/src/responders/responder-validators.test.js b/keyserver/src/responders/responder-validators.test.js index e0f8dd93d..185f5e1c0 100644 --- a/keyserver/src/responders/responder-validators.test.js +++ b/keyserver/src/responders/responder-validators.test.js @@ -1,976 +1,971 @@ // @flow import { setThreadUnreadStatusResultValidator, updateActivityResultValidator, } from 'lib/types/activity-types.js'; import { fetchEntryInfosResponseValidator, fetchEntryRevisionInfosResultValidator, saveEntryResponseValidator, deleteEntryResponseValidator, deltaEntryInfosResultValidator, restoreEntryResponseValidator, } from './entry-responders.js'; import { getSessionPublicKeysResponseValidator } from './keys-responders.js'; import { inviteLinkVerificationResponseValidator, fetchInviteLinksResponseValidator, } from './link-responders.js'; import { messageReportCreationResultValidator } from './message-report-responder.js'; import { fetchMessageInfosResponseValidator, fetchPinnedMessagesResultValidator, sendEditMessageResponseValidator, sendMessageResponseValidator, } from './message-responders.js'; import { relationshipErrorsValidator } from './relationship-responders.js'; import { reportCreationResponseValidator } from './report-responders.js'; import { userSearchResultValidator } from './search-responders.js'; import { siweNonceResponseValidator } from './siwe-nonce-responders.js'; import { changeThreadSettingsResultValidator, leaveThreadResultValidator, newThreadResponseValidator, threadFetchMediaResultValidator, threadJoinResultValidator, toggleMessagePinResultValidator, roleChangeRequestInputValidator, } from './thread-responders.js'; import { logInResponseValidator, registerResponseValidator, logOutResponseValidator, } from './user-responders.js'; describe('user responder validators', () => { it('should validate logout response', () => { const response = { currentUserInfo: { id: '93078', anonymous: true } }; expect(logOutResponseValidator.is(response)).toBe(true); response.currentUserInfo.anonymous = false; expect(logOutResponseValidator.is(response)).toBe(false); }); it('should validate register response', () => { const response = { id: '93079', rawMessageInfos: [ { type: 1, threadID: '93095', creatorID: '93079', time: 1682086407469, initialThreadState: { type: 6, name: null, parentThreadID: '1', color: '648caa', memberIDs: ['256', '93079'], }, id: '93110', }, { type: 0, threadID: '93095', creatorID: '256', time: 1682086407575, text: 'welcome to Comm!', id: '93113', }, ], currentUserInfo: { id: '93079', username: 'user' }, cookieChange: { threadInfos: { '1': { id: '1', type: 12, name: 'GENESIS', description: 'desc', color: 'c85000', creationTime: 1672934346213, parentThreadID: null, members: [ { id: '256', role: '83796', permissions: { know_of: { value: true, source: '1' }, - membership: { value: false, source: null }, 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: false, source: null }, edit_permissions: { value: false, source: null }, 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: true, source: '1' }, edit_message: { value: true, source: '1' }, }, isSender: false, }, ], roles: { '83795': { id: '83795', name: 'Members', permissions: { know_of: true, visible: true, descendant_open_know_of: true, descendant_open_visible: true, descendant_opentoplevel_join_thread: true, }, isDefault: true, }, }, currentUser: { role: '83795', permissions: { know_of: { value: true, source: '1' }, - membership: { value: false, source: null }, visible: { value: true, source: '1' }, voiced: { value: false, source: null }, edit_entries: { value: false, source: null }, edit_thread: { value: false, source: null }, edit_thread_description: { value: false, source: null }, edit_thread_color: { value: false, source: null }, delete_thread: { value: false, source: null }, create_subthreads: { value: false, source: null }, create_sidebars: { value: false, 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: false, source: null }, edit_message: { value: false, source: null }, }, subscription: { home: true, pushNotifs: true }, unread: true, }, repliesCount: 0, containingThreadID: null, community: null, }, }, userInfos: [ { id: '5', username: 'commbot' }, { id: '256', username: 'ashoat' }, { id: '93079', username: 'temp_user7' }, ], }, }; expect(registerResponseValidator.is(response)).toBe(true); response.cookieChange.userInfos = undefined; expect(registerResponseValidator.is(response)).toBe(false); }); it('should validate login response', () => { const response = { currentUserInfo: { id: '93079', username: 'temp_user7' }, rawMessageInfos: [ { type: 0, id: '93115', threadID: '93094', time: 1682086407577, creatorID: '5', text: 'This is your private chat, where you can set', }, { type: 1, id: '93111', threadID: '93094', time: 1682086407467, creatorID: '93079', initialThreadState: { type: 7, name: 'temp_user7', parentThreadID: '1', color: '575757', memberIDs: ['93079'], }, }, ], truncationStatuses: { '93094': 'exhaustive', '93095': 'exhaustive' }, serverTime: 1682086579416, userInfos: [ { id: '5', username: 'commbot' }, { id: '256', username: 'ashoat' }, { id: '93079', username: 'temp_user7' }, ], cookieChange: { threadInfos: { '1': { id: '1', type: 12, name: 'GENESIS', description: 'This is the first community on Comm. In the future it will', color: 'c85000', creationTime: 1672934346213, parentThreadID: null, members: [ { id: '256', role: '83796', permissions: { know_of: { value: true, source: '1' }, - membership: { value: false, source: null }, 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: false, source: null }, edit_permissions: { value: false, source: null }, 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: true, source: '1' }, edit_message: { value: true, source: '1' }, }, isSender: false, }, { id: '93079', role: '83795', permissions: { know_of: { value: true, source: '1' }, - membership: { value: false, source: null }, visible: { value: true, source: '1' }, voiced: { value: false, source: null }, edit_entries: { value: false, source: null }, edit_thread: { value: false, source: null }, edit_thread_description: { value: false, source: null }, edit_thread_color: { value: false, source: null }, delete_thread: { value: false, source: null }, create_subthreads: { value: false, source: null }, create_sidebars: { value: false, 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: false, source: null }, edit_message: { value: false, source: null }, }, isSender: false, }, ], roles: { '83795': { id: '83795', name: 'Members', permissions: { know_of: true, visible: true, descendant_open_know_of: true, descendant_open_visible: true, descendant_opentoplevel_join_thread: true, }, isDefault: true, }, '83796': { id: '83796', name: 'Admins', 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_subthreads: true, create_sidebars: true, add_members: true, delete_thread: true, remove_members: true, change_role: true, descendant_know_of: true, descendant_visible: true, descendant_toplevel_join_thread: true, child_join_thread: true, descendant_voiced: true, descendant_edit_entries: true, descendant_edit_thread: true, descendant_edit_thread_color: true, descendant_edit_thread_description: true, descendant_toplevel_create_subthreads: true, descendant_toplevel_create_sidebars: true, descendant_add_members: true, descendant_delete_thread: true, descendant_edit_permissions: true, descendant_remove_members: true, descendant_change_role: true, }, isDefault: false, }, }, currentUser: { role: '83795', permissions: { know_of: { value: true, source: '1' }, - membership: { value: false, source: null }, visible: { value: true, source: '1' }, voiced: { value: false, source: null }, edit_entries: { value: false, source: null }, edit_thread: { value: false, source: null }, edit_thread_description: { value: false, source: null }, edit_thread_color: { value: false, source: null }, delete_thread: { value: false, source: null }, create_subthreads: { value: false, source: null }, create_sidebars: { value: false, 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: false, source: null }, edit_message: { value: false, source: null }, }, subscription: { home: true, pushNotifs: true }, unread: true, }, repliesCount: 0, containingThreadID: null, community: null, }, }, userInfos: [], }, rawEntryInfos: [], }; expect(logInResponseValidator.is(response)).toBe(true); expect( logInResponseValidator.is({ ...response, currentUserInfo: undefined }), ).toBe(false); }); }); describe('search responder', () => { it('should validate search response', () => { const response = { userInfos: [ { id: '83817', username: 'temp_user0' }, { id: '83853', username: 'temp_user1' }, { id: '83890', username: 'temp_user2' }, { id: '83928', username: 'temp_user3' }, ], }; expect(userSearchResultValidator.is(response)).toBe(true); response.userInfos.push({ id: 123 }); expect(userSearchResultValidator.is(response)).toBe(false); }); }); describe('message report responder', () => { it('should validate message report response', () => { const response = { messageInfo: { type: 0, threadID: '101113', creatorID: '5', time: 1682429699746, text: 'text', id: '101121', }, }; expect(messageReportCreationResultValidator.is(response)).toBe(true); response.messageInfo.type = -2; expect(messageReportCreationResultValidator.is(response)).toBe(false); }); }); describe('relationship responder', () => { it('should validate relationship response', () => { const response = { invalid_user: ['83817', '83890'], already_friends: ['83890'], }; expect(relationshipErrorsValidator.is(response)).toBe(true); expect( relationshipErrorsValidator.is({ ...response, user_blocked: {} }), ).toBe(false); }); }); describe('activity responder', () => { it('should validate update activity response', () => { const response = { unfocusedToUnread: ['93095'] }; expect(updateActivityResultValidator.is(response)).toBe(true); response.unfocusedToUnread.push(123); expect(updateActivityResultValidator.is(response)).toBe(false); }); it('should validate set thread unread response', () => { const response = { resetToUnread: false }; expect(setThreadUnreadStatusResultValidator.is(response)).toBe(true); expect( setThreadUnreadStatusResultValidator.is({ ...response, unread: false }), ).toBe(false); }); }); describe('keys responder', () => { it('should validate get session public keys response', () => { const response = { identityKey: 'key', oneTimeKey: 'key', }; expect(getSessionPublicKeysResponseValidator.is(response)).toBe(true); expect(getSessionPublicKeysResponseValidator.is(null)).toBe(true); expect( getSessionPublicKeysResponseValidator.is({ ...response, identityKey: undefined, }), ).toBe(false); }); }); describe('siwe nonce responders', () => { it('should validate siwe nonce response', () => { const response = { nonce: 'nonce' }; expect(siweNonceResponseValidator.is(response)).toBe(true); expect(siweNonceResponseValidator.is({ nonce: 123 })).toBe(false); }); }); describe('entry reponders', () => { it('should validate entry fetch response', () => { const response = { rawEntryInfos: [ { id: '92860', threadID: '85068', text: 'text', year: 2023, month: 4, day: 2, creationTime: 1682082939882, creatorID: '83853', deleted: false, }, ], userInfos: { '123': { id: '123', username: 'username', }, }, }; expect(fetchEntryInfosResponseValidator.is(response)).toBe(true); expect( fetchEntryInfosResponseValidator.is({ ...response, userInfos: undefined, }), ).toBe(false); }); it('should validate entry revision fetch response', () => { const response = { result: [ { id: '93297', authorID: '83853', text: 'text', lastUpdate: 1682603494202, deleted: false, threadID: '83859', entryID: '93270', }, { id: '93284', authorID: '83853', text: 'text', lastUpdate: 1682603426996, deleted: true, threadID: '83859', entryID: '93270', }, ], }; expect(fetchEntryRevisionInfosResultValidator.is(response)).toBe(true); expect( fetchEntryRevisionInfosResultValidator.is({ ...response, result: {}, }), ).toBe(false); }); it('should validate entry save response', () => { const response = { entryID: '93270', newMessageInfos: [ { type: 9, threadID: '83859', creatorID: '83853', time: 1682603362817, entryID: '93270', date: '2023-04-03', text: 'text', id: '93272', }, ], updatesResult: { viewerUpdates: [], userInfos: [] }, }; expect(saveEntryResponseValidator.is(response)).toBe(true); expect( saveEntryResponseValidator.is({ ...response, entryID: undefined, }), ).toBe(false); }); it('should validate entry delete response', () => { const response = { threadID: '83859', newMessageInfos: [ { type: 11, threadID: '83859', creatorID: '83853', time: 1682603427038, entryID: '93270', date: '2023-04-03', text: 'text', id: '93285', }, ], updatesResult: { viewerUpdates: [], userInfos: [] }, }; expect(deleteEntryResponseValidator.is(response)).toBe(true); expect( deleteEntryResponseValidator.is({ ...response, threadID: undefined, }), ).toBe(false); }); it('should validate entry restore response', () => { const response = { newMessageInfos: [ { type: 11, threadID: '83859', creatorID: '83853', time: 1682603427038, entryID: '93270', date: '2023-04-03', text: 'text', id: '93285', }, ], updatesResult: { viewerUpdates: [], userInfos: [] }, }; expect(restoreEntryResponseValidator.is(response)).toBe(true); expect( restoreEntryResponseValidator.is({ ...response, newMessageInfos: undefined, }), ).toBe(false); }); it('should validate entry delta response', () => { const response = { rawEntryInfos: [ { id: '92860', threadID: '85068', text: 'text', year: 2023, month: 4, day: 2, creationTime: 1682082939882, creatorID: '83853', deleted: false, }, ], deletedEntryIDs: ['92860'], userInfos: [ { id: '123', username: 'username', }, ], }; expect(deltaEntryInfosResultValidator.is(response)).toBe(true); expect( deltaEntryInfosResultValidator.is({ ...response, rawEntryInfos: undefined, }), ).toBe(false); }); }); describe('thread responders', () => { it('should validate change thread settings response', () => { const response = { updatesResult: { newUpdates: [ { type: 1, id: '93601', time: 1682759546258, threadInfo: { id: '92796', type: 6, name: '', description: '', color: 'b8753d', creationTime: 1682076700918, parentThreadID: '1', members: [], roles: {}, currentUser: { role: '85172', permissions: {}, subscription: { home: true, pushNotifs: true, }, unread: false, }, repliesCount: 0, containingThreadID: '1', community: '1', pinnedCount: 0, }, }, ], }, newMessageInfos: [ { type: 4, threadID: '92796', creatorID: '83928', time: 1682759546275, field: 'color', value: 'b8753d', id: '93602', }, ], }; expect(changeThreadSettingsResultValidator.is(response)).toBe(true); expect( changeThreadSettingsResultValidator.is({ ...response, newMessageInfos: undefined, }), ).toBe(false); }); it('should validate leave thread response', () => { const response = { updatesResult: { newUpdates: [ { type: 3, id: '93595', time: 1682759498811, threadID: '93561' }, ], }, }; expect(leaveThreadResultValidator.is(response)).toBe(true); expect( leaveThreadResultValidator.is({ ...response, updatedResult: undefined, }), ).toBe(false); }); it('should validate new thread response', () => { const response = { newThreadID: '93619', updatesResult: { newUpdates: [ { type: 4, id: '93621', time: 1682759805331, threadInfo: { id: '93619', type: 5, name: 'a', description: '', color: 'b8753d', creationTime: 1682759805298, parentThreadID: '92796', members: [], roles: {}, currentUser: { role: '85172', permissions: {}, subscription: { home: true, pushNotifs: true, }, unread: false, }, repliesCount: 0, containingThreadID: '92796', community: '1', sourceMessageID: '93614', pinnedCount: 0, }, rawMessageInfos: [], truncationStatus: 'exhaustive', rawEntryInfos: [], }, ], }, userInfos: { '256': { id: '256', username: 'ashoat' }, '83928': { id: '83928', username: 'temp_user3' }, }, newMessageInfos: [], }; expect(newThreadResponseValidator.is(response)).toBe(true); expect( newThreadResponseValidator.is({ ...response, newMessageInfos: {}, }), ).toBe(false); }); it('should validate thread join response', () => { const response = { rawMessageInfos: [ { type: 8, threadID: '93619', creatorID: '83928', time: 1682759915935, id: '93640', }, ], truncationStatuses: {}, userInfos: { '256': { id: '256', username: 'ashoat' }, '83928': { id: '83928', username: 'temp_user3' }, }, updatesResult: { newUpdates: [], }, }; expect(threadJoinResultValidator.is(response)).toBe(true); expect( threadJoinResultValidator.is({ ...response, updatesResult: [], }), ).toBe(false); }); it('should validate thread fetch media response', () => { const response = { media: [ { type: 'photo', id: '93642', uri: 'http://0.0.0.0:3000/comm/upload/93642/1e0d7a5262952e3b', dimensions: { width: 220, height: 220 }, }, ], }; expect(threadFetchMediaResultValidator.is(response)).toBe(true); expect( threadFetchMediaResultValidator.is({ ...response, media: undefined }), ).toBe(false); }); it('should validate toggle message pin response', () => { const response = { threadID: '123', newMessageInfos: [] }; expect(toggleMessagePinResultValidator.is(response)).toBe(true); expect( toggleMessagePinResultValidator.is({ ...response, threadID: undefined }), ).toBe(false); }); it('should validate role change request input', () => { const input = { threadID: '123', memberIDs: [], role: '1', }; expect(roleChangeRequestInputValidator.is(input)).toBe(true); expect(roleChangeRequestInputValidator.is({ ...input, role: '2|1' })).toBe( true, ); expect(roleChangeRequestInputValidator.is({ ...input, role: '-1' })).toBe( false, ); expect(roleChangeRequestInputValidator.is({ ...input, role: '2|-1' })).toBe( false, ); }); }); describe('message responders', () => { it('should validate send message response', () => { const response = { newMessageInfo: { type: 0, threadID: '93619', creatorID: '83928', time: 1682761023640, text: 'a', localID: 'local3', id: '93649', }, }; expect(sendMessageResponseValidator.is(response)).toBe(true); expect( sendMessageResponseValidator.is({ ...response, newMEssageInfos: undefined, }), ).toBe(false); }); it('should validate fetch message infos response', () => { const response = { rawMessageInfos: [ { type: 0, id: '83954', threadID: '83938', time: 1673561155110, creatorID: '256', text: 'welcome to Comm!', }, ], truncationStatuses: { '83938': 'exhaustive' }, userInfos: { '256': { id: '256', username: 'ashoat' }, '83928': { id: '83928', username: 'temp_user3' }, }, }; expect(fetchMessageInfosResponseValidator.is(response)).toBe(true); expect( fetchMessageInfosResponseValidator.is({ ...response, userInfos: undefined, }), ).toBe(false); }); it('should validate send edit message response', () => { const response = { newMessageInfos: [ { type: 0, id: '83954', threadID: '83938', time: 1673561155110, creatorID: '256', text: 'welcome to Comm!', }, ], }; expect(sendEditMessageResponseValidator.is(response)).toBe(true); expect( sendEditMessageResponseValidator.is({ ...response, newMessageInfos: undefined, }), ).toBe(false); }); it('should validate fetch pinned message response', () => { const response = { pinnedMessages: [ { type: 0, id: '83954', threadID: '83938', time: 1673561155110, creatorID: '256', text: 'welcome to Comm!', }, ], }; expect(fetchPinnedMessagesResultValidator.is(response)).toBe(true); expect( fetchPinnedMessagesResultValidator.is({ ...response, pinnedMessages: undefined, }), ).toBe(false); }); }); describe('report responders', () => { it('should validate report creation response', () => { const response = { id: '123' }; expect(reportCreationResponseValidator.is(response)).toBe(true); expect(reportCreationResponseValidator.is({})).toBe(false); }); }); describe('link responders', () => { it('should validate invite link verification response', () => { const response = { status: 'already_joined', community: { name: 'name', id: '123', }, }; expect(inviteLinkVerificationResponseValidator.is(response)).toBe(true); expect(inviteLinkVerificationResponseValidator.is({})).toBe(false); }); it('should validate invite link verification response', () => { const response = { links: [ { name: 'name', primary: true, role: '123', communityID: '123', expirationTime: 123, limitOfUses: 123, numberOfUses: 123, }, ], }; expect(fetchInviteLinksResponseValidator.is(response)).toBe(true); expect(fetchInviteLinksResponseValidator.is({ links: {} })).toBe(false); }); }); diff --git a/lib/permissions/minimally-encoded-thread-permissions.js b/lib/permissions/minimally-encoded-thread-permissions.js new file mode 100644 index 000000000..078dd7832 --- /dev/null +++ b/lib/permissions/minimally-encoded-thread-permissions.js @@ -0,0 +1,78 @@ +// @flow + +import invariant from 'invariant'; + +import type { + ThreadPermission, + ThreadPermissionsInfo, +} from '../types/thread-permission-types.js'; +import { entries } from '../utils/objects.js'; + +const minimallyEncodedThreadPermissions = Object.freeze({ + // TODO (atul): Update flow to `194.0.0` for bigint support + // $FlowIssue bigint-unsupported + know_of: BigInt(1) << BigInt(0), + visible: BigInt(1) << BigInt(1), + voiced: BigInt(1) << BigInt(2), + edit_entries: BigInt(1) << BigInt(3), + edit_thread: BigInt(1) << BigInt(4), // EDIT_THREAD_NAME + edit_thread_description: BigInt(1) << BigInt(5), + edit_thread_color: BigInt(1) << BigInt(6), + delete_thread: BigInt(1) << BigInt(7), + create_subthreads: BigInt(1) << BigInt(8), // CREATE_SUBCHANNELS + create_sidebars: BigInt(1) << BigInt(9), + join_thread: BigInt(1) << BigInt(10), + edit_permissions: BigInt(1) << BigInt(11), + add_members: BigInt(1) << BigInt(12), + remove_members: BigInt(1) << BigInt(13), + change_role: BigInt(1) << BigInt(14), + leave_thread: BigInt(1) << BigInt(15), + react_to_message: BigInt(1) << BigInt(16), + edit_message: BigInt(1) << BigInt(17), + edit_thread_avatar: BigInt(1) << BigInt(18), + manage_pins: BigInt(1) << BigInt(19), + manage_invite_links: BigInt(1) << BigInt(20), +}); + +// TODO (atul): Update flow to `194.0.0` for bigint support +// $FlowIssue bigint-unsupported +const permissionsToBitmask = (permissions: ThreadPermissionsInfo): bigint => { + let bitmask = BigInt(0); + for (const [key, permission] of entries(permissions)) { + if (permission.value && key in minimallyEncodedThreadPermissions) { + invariant( + // TODO (atul): Update flow to `194.0.0` for bigint support + // $FlowIssue illegal-typeof + typeof minimallyEncodedThreadPermissions[key] === 'bigint', + 'must be bigint', + ); + bitmask |= minimallyEncodedThreadPermissions[key]; + } + } + return bitmask; +}; + +const hasPermission = ( + // TODO (atul): Update flow to `194.0.0` for bigint support + // $FlowIssue bigint-unsupported + permissionsBitmask: bigint, + permission: ThreadPermission, +): boolean => { + if (!(permission in minimallyEncodedThreadPermissions)) { + return false; + } + const permissionBitmask = minimallyEncodedThreadPermissions[permission]; + invariant( + // TODO (atul): Update flow to `194.0.0` for bigint support + // $FlowIssue illegal-typeof + typeof permissionBitmask === 'bigint', + 'permissionBitmask must be of type bigint', + ); + return (permissionsBitmask & permissionBitmask) !== BigInt(0); +}; + +export { + minimallyEncodedThreadPermissions, + permissionsToBitmask, + hasPermission, +}; diff --git a/lib/permissions/minimally-encoded-thread-permissions.test.js b/lib/permissions/minimally-encoded-thread-permissions.test.js new file mode 100644 index 000000000..741926a41 --- /dev/null +++ b/lib/permissions/minimally-encoded-thread-permissions.test.js @@ -0,0 +1,53 @@ +// @flow + +import { + hasPermission, + permissionsToBitmask, +} from './minimally-encoded-thread-permissions.js'; + +describe('minimallyEncodedThreadPermissions', () => { + const 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: false, source: null }, + edit_permissions: { value: false, source: null }, + 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: true, source: '1' }, + edit_message: { value: true, source: '1' }, + }; + + it('should encode ThreadPermissionsInfo as bitmask', () => { + const permissionsBitmask = permissionsToBitmask(permissions); + expect(permissionsBitmask.toString(2)).toBe('110111001111111111'); + expect(hasPermission(permissionsBitmask, 'know_of')).toBe(true); + expect(hasPermission(permissionsBitmask, 'visible')).toBe(true); + expect(hasPermission(permissionsBitmask, 'voiced')).toBe(true); + expect(hasPermission(permissionsBitmask, 'edit_entries')).toBe(true); + expect(hasPermission(permissionsBitmask, 'edit_thread')).toBe(true); + expect(hasPermission(permissionsBitmask, 'edit_thread_description')).toBe( + true, + ); + expect(hasPermission(permissionsBitmask, 'edit_thread_color')).toBe(true); + expect(hasPermission(permissionsBitmask, 'delete_thread')).toBe(true); + expect(hasPermission(permissionsBitmask, 'create_subthreads')).toBe(true); + expect(hasPermission(permissionsBitmask, 'create_sidebars')).toBe(true); + expect(hasPermission(permissionsBitmask, 'join_thread')).toBe(false); + expect(hasPermission(permissionsBitmask, 'edit_permissions')).toBe(false); + expect(hasPermission(permissionsBitmask, 'remove_members')).toBe(true); + expect(hasPermission(permissionsBitmask, 'change_role')).toBe(true); + expect(hasPermission(permissionsBitmask, 'leave_thread')).toBe(false); + expect(hasPermission(permissionsBitmask, 'react_to_message')).toBe(true); + expect(hasPermission(permissionsBitmask, 'edit_message')).toBe(true); + }); +}); diff --git a/lib/types/thread-permission-types.js b/lib/types/thread-permission-types.js index 6c2bd764d..d4a7f7b3c 100644 --- a/lib/types/thread-permission-types.js +++ b/lib/types/thread-permission-types.js @@ -1,429 +1,427 @@ // @flow import invariant from 'invariant'; import t, { type TDict, type TUnion, type TEnums } from 'tcomb'; import { values } from '../utils/objects.js'; import { tBool, tShape, tID } from '../utils/validation-utils.js'; // When a new permission is added, if it should be configurable for a role, it // should be either added to an existing set or a new set alongside a // new user-facing permission. If it is a permission that should be ensured // across all roles, it should be added to `universalCommunityPermissions`. export const threadPermissions = Object.freeze({ KNOW_OF: 'know_of', - MEMBERSHIP_DEPRECATED: 'membership', VISIBLE: 'visible', VOICED: 'voiced', EDIT_ENTRIES: 'edit_entries', EDIT_THREAD_NAME: 'edit_thread', EDIT_THREAD_DESCRIPTION: 'edit_thread_description', EDIT_THREAD_COLOR: 'edit_thread_color', DELETE_THREAD: 'delete_thread', CREATE_SUBCHANNELS: 'create_subthreads', CREATE_SIDEBARS: 'create_sidebars', JOIN_THREAD: 'join_thread', EDIT_PERMISSIONS: 'edit_permissions', ADD_MEMBERS: 'add_members', REMOVE_MEMBERS: 'remove_members', CHANGE_ROLE: 'change_role', LEAVE_THREAD: 'leave_thread', REACT_TO_MESSAGE: 'react_to_message', EDIT_MESSAGE: 'edit_message', EDIT_THREAD_AVATAR: 'edit_thread_avatar', MANAGE_PINS: 'manage_pins', MANAGE_INVITE_LINKS: 'manage_invite_links', }); export type ThreadPermission = $Values; export function assertThreadPermissions( ourThreadPermissions: string, ): ThreadPermission { invariant( ourThreadPermissions === 'know_of' || - ourThreadPermissions === 'membership' || ourThreadPermissions === 'visible' || ourThreadPermissions === 'voiced' || ourThreadPermissions === 'edit_entries' || ourThreadPermissions === 'edit_thread' || ourThreadPermissions === 'edit_thread_description' || ourThreadPermissions === 'edit_thread_color' || ourThreadPermissions === 'delete_thread' || ourThreadPermissions === 'create_subthreads' || ourThreadPermissions === 'create_sidebars' || ourThreadPermissions === 'join_thread' || ourThreadPermissions === 'edit_permissions' || ourThreadPermissions === 'add_members' || ourThreadPermissions === 'remove_members' || ourThreadPermissions === 'change_role' || ourThreadPermissions === 'leave_thread' || ourThreadPermissions === 'react_to_message' || ourThreadPermissions === 'edit_message' || ourThreadPermissions === 'edit_thread_avatar' || ourThreadPermissions === 'manage_pins' || ourThreadPermissions === 'manage_invite_links', 'string is not threadPermissions enum', ); return ourThreadPermissions; } const threadPermissionValidator = t.enums.of(values(threadPermissions)); export const threadPermissionPropagationPrefixes = Object.freeze({ DESCENDANT: 'descendant_', CHILD: 'child_', }); export type ThreadPermissionPropagationPrefix = $Values< typeof threadPermissionPropagationPrefixes, >; export const threadPermissionFilterPrefixes = Object.freeze({ // includes only SIDEBAR, COMMUNITY_OPEN_SUBTHREAD, // COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD OPEN: 'open_', // excludes only SIDEBAR TOP_LEVEL: 'toplevel_', // includes only COMMUNITY_OPEN_SUBTHREAD, // COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD OPEN_TOP_LEVEL: 'opentoplevel_', }); export type ThreadPermissionFilterPrefix = $Values< typeof threadPermissionFilterPrefixes, >; // These are the set of user-facing permissions that we display as configurable // to the user when they are creating a custom role for their given community. // They are per-community rather than per-thread, so when configured they are // to be expected to be propagated across the community. Also notably, // `threadPermissions` is used on the keyserver for permission checks to // validate actions, but these `userSurfacedPermissions` are only used // on the client for the UI and propagated to the server. The // `configurableCommunityPermissions` mapping below is the association between // each userSurfacedPermission and a set of threadPermissions. export const userSurfacedPermissions = Object.freeze({ EDIT_CALENDAR: 'edit_calendar', KNOW_OF_SECRET_CHANNELS: 'know_of_secret_channels', VOICED_IN_ANNOUNCEMENT_CHANNELS: 'voiced_in_announcement_channels', CREATE_AND_EDIT_CHANNELS: 'create_and_edit_channels', DELETE_CHANNELS: 'delete_channels', ADD_MEMBERS: 'add_members', REMOVE_MEMBERS: 'remove_members', CHANGE_ROLES: 'change_roles', EDIT_VISIBILITY: 'edit_visibility', MANAGE_PINS: 'manage_pins', REACT_TO_MESSAGES: 'react_to_messages', EDIT_MESSAGES: 'edit_messages', MANAGE_INVITE_LINKS: 'manage_invite_links', }); export type UserSurfacedPermission = $Values; export const userSurfacedPermissionsSet: $ReadOnlySet = new Set(values(userSurfacedPermissions)); export const userSurfacedPermissionValidator: TEnums = t.enums.of( values(userSurfacedPermissions), ); const editCalendarPermission = { title: 'Edit calendar', description: 'Allows members to edit the community calendar', userSurfacedPermission: userSurfacedPermissions.EDIT_CALENDAR, }; const editEntries = threadPermissions.EDIT_ENTRIES; const descendantEditEntries = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_ENTRIES; const editCalendarPermissions = new Set([editEntries, descendantEditEntries]); const knowOfSecretChannelsPermission = { title: 'Know of secret channels', description: 'Allows members to know of all secret channels', userSurfacedPermission: userSurfacedPermissions.KNOW_OF_SECRET_CHANNELS, }; const descendantKnowOf = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.KNOW_OF; const descendantVisible = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.VISIBLE; const descendantTopLevelJoinThread = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.TOP_LEVEL + threadPermissions.JOIN_THREAD; const descendantJoinThread = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.JOIN_THREAD; const childJoinThread = threadPermissionPropagationPrefixes.CHILD + threadPermissions.JOIN_THREAD; const knowOfSecretChannelsPermissions = new Set([ descendantKnowOf, descendantVisible, descendantTopLevelJoinThread, descendantJoinThread, childJoinThread, ]); const voicedPermission = { title: 'Voiced in announcement channels', description: 'Allows members to send messages in announcement channels', userSurfacedPermission: userSurfacedPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS, }; const voiced = threadPermissions.VOICED; const voicedPermissions = new Set([voiced]); const createAndEditChannelsPermission = { title: 'Create and edit channels', description: 'Allows members to create new and edit existing channels', userSurfacedPermission: userSurfacedPermissions.CREATE_AND_EDIT_CHANNELS, }; const editThreadName = threadPermissions.EDIT_THREAD_NAME; const descendantEditThreadName = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_THREAD_NAME; const editThreadDescription = threadPermissions.EDIT_THREAD_DESCRIPTION; const descendantEditThreadDescription = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_THREAD_DESCRIPTION; const editThreadColor = threadPermissions.EDIT_THREAD_COLOR; const descendantEditThreadColor = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_THREAD_COLOR; const createSubchannels = threadPermissions.CREATE_SUBCHANNELS; const descendantCreateSubchannels = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.TOP_LEVEL + threadPermissions.CREATE_SUBCHANNELS; const editThreadAvatar = threadPermissions.EDIT_THREAD_AVATAR; const descendantEditThreadAvatar = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_THREAD_AVATAR; const descendantTopLevelCreateSidebars = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.TOP_LEVEL + threadPermissions.CREATE_SIDEBARS; const createAndEditChannelsPermissions = new Set([ editThreadName, descendantEditThreadName, editThreadDescription, descendantEditThreadDescription, editThreadColor, descendantEditThreadColor, createSubchannels, descendantCreateSubchannels, editThreadAvatar, descendantEditThreadAvatar, descendantTopLevelCreateSidebars, ]); const deleteChannelsPermission = { title: 'Delete channels', description: 'Allows members to delete channels', userSurfacedPermission: userSurfacedPermissions.DELETE_CHANNELS, }; const deleteThread = threadPermissions.DELETE_THREAD; const descendantDeleteThread = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.DELETE_THREAD; const deleteChannelsPermissions = new Set([ deleteThread, descendantDeleteThread, ]); const addMembersPermission = { title: 'Add members', description: 'Allows members to add other members to channels', userSurfacedPermission: userSurfacedPermissions.ADD_MEMBERS, }; const addMembers = threadPermissions.ADD_MEMBERS; const descendantAddMembers = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.ADD_MEMBERS; const childOpenAddMembers = threadPermissionPropagationPrefixes.CHILD + threadPermissionFilterPrefixes.OPEN + threadPermissions.ADD_MEMBERS; const addMembersPermissions = new Set([ addMembers, descendantAddMembers, childOpenAddMembers, ]); const removeMembersPermission = { title: 'Remove members', description: 'Allows members to remove anybody they can demote from channels', userSurfacedPermission: userSurfacedPermissions.REMOVE_MEMBERS, }; const removeMembers = threadPermissions.REMOVE_MEMBERS; const descendantRemoveMembers = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.REMOVE_MEMBERS; const removeMembersPermissions = new Set([ removeMembers, descendantRemoveMembers, ]); const changeRolePermission = { title: 'Change roles', description: 'Allows members to promote and demote other members', userSurfacedPermission: userSurfacedPermissions.CHANGE_ROLES, }; const changeRole = threadPermissions.CHANGE_ROLE; const descendantChangeRole = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.CHANGE_ROLE; const changeRolePermissions = new Set([changeRole, descendantChangeRole]); const editVisibilityPermission = { title: 'Edit visibility', description: 'Allows members to edit visibility permissions of channels', userSurfacedPermission: userSurfacedPermissions.EDIT_VISIBILITY, }; const editPermissions = threadPermissions.EDIT_PERMISSIONS; const descendantEditPermissions = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_PERMISSIONS; const editVisibilityPermissions = new Set([ editPermissions, descendantEditPermissions, ]); const managePinsPermission = { title: 'Manage pins', description: 'Allows members to pin or unpin messages in channels', userSurfacedPermission: userSurfacedPermissions.MANAGE_PINS, }; const managePins = threadPermissions.MANAGE_PINS; const descendantManagePins = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.MANAGE_PINS; const managePinsPermissions = new Set([managePins, descendantManagePins]); const reactToMessagePermission = { title: 'React to messages', description: 'Allows members to add reactions to messages', userSurfacedPermission: userSurfacedPermissions.REACT_TO_MESSAGES, }; const reactToMessage = threadPermissions.REACT_TO_MESSAGE; const descendantReactToMessage = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.REACT_TO_MESSAGE; const reactToMessagePermissions = new Set([ reactToMessage, descendantReactToMessage, ]); const editMessagePermission = { title: 'Edit messages', description: 'Allows members to edit their sent messages', userSurfacedPermission: userSurfacedPermissions.EDIT_MESSAGES, }; const editMessage = threadPermissions.EDIT_MESSAGE; const descendantEditMessage = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.EDIT_MESSAGE; const editMessagePermissions = new Set([editMessage, descendantEditMessage]); const manageInviteLinksPermission = { title: 'Manage invite links', description: 'Allows members to create and delete invite links', userSurfacedPermission: userSurfacedPermissions.MANAGE_INVITE_LINKS, }; const manageInviteLinks = threadPermissions.MANAGE_INVITE_LINKS; const descendantManageInviteLinks = threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.MANAGE_INVITE_LINKS; const manageInviteLinksPermissions = new Set([ manageInviteLinks, descendantManageInviteLinks, ]); export type UserSurfacedPermissionOption = { +title: string, +description: string, +userSurfacedPermission: UserSurfacedPermission, }; export const userSurfacedPermissionOptions: $ReadOnlySet = new Set([ editCalendarPermission, knowOfSecretChannelsPermission, voicedPermission, createAndEditChannelsPermission, deleteChannelsPermission, addMembersPermission, removeMembersPermission, changeRolePermission, editVisibilityPermission, managePinsPermission, reactToMessagePermission, editMessagePermission, manageInviteLinksPermission, ]); type ConfigurableCommunityPermission = { +[permission: UserSurfacedPermission]: $ReadOnlySet, }; export const configurableCommunityPermissions: ConfigurableCommunityPermission = Object.freeze({ [userSurfacedPermissions.EDIT_CALENDAR]: editCalendarPermissions, [userSurfacedPermissions.KNOW_OF_SECRET_CHANNELS]: knowOfSecretChannelsPermissions, [userSurfacedPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS]: voicedPermissions, [userSurfacedPermissions.CREATE_AND_EDIT_CHANNELS]: createAndEditChannelsPermissions, [userSurfacedPermissions.DELETE_CHANNELS]: deleteChannelsPermissions, [userSurfacedPermissions.ADD_MEMBERS]: addMembersPermissions, [userSurfacedPermissions.REMOVE_MEMBERS]: removeMembersPermissions, [userSurfacedPermissions.CHANGE_ROLES]: changeRolePermissions, [userSurfacedPermissions.EDIT_VISIBILITY]: editVisibilityPermissions, [userSurfacedPermissions.MANAGE_PINS]: managePinsPermissions, [userSurfacedPermissions.REACT_TO_MESSAGES]: reactToMessagePermissions, [userSurfacedPermissions.EDIT_MESSAGES]: editMessagePermissions, [userSurfacedPermissions.MANAGE_INVITE_LINKS]: manageInviteLinksPermissions, }); export const universalCommunityPermissions: $ReadOnlyArray = [ // know_of | descendant_open_know_of threadPermissions.KNOW_OF, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.OPEN + threadPermissions.KNOW_OF, // descendant_open_voiced threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.OPEN + threadPermissions.VOICED, // visible | descendant_open_visible threadPermissions.VISIBLE, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.OPEN + threadPermissions.VISIBLE, // join_thread | child_open_join_thread | descendant_opentoplevel_join_thread threadPermissions.JOIN_THREAD, threadPermissionPropagationPrefixes.CHILD + threadPermissionFilterPrefixes.OPEN + threadPermissions.JOIN_THREAD, threadPermissionPropagationPrefixes.DESCENDANT + threadPermissionFilterPrefixes.OPEN_TOP_LEVEL + threadPermissions.JOIN_THREAD, threadPermissions.CREATE_SIDEBARS, threadPermissions.LEAVE_THREAD, ]; export type ThreadPermissionInfo = | { +value: true, +source: string } | { +value: false, +source: null }; export const threadPermissionInfoValidator: TUnion = t.union([ tShape({ value: tBool(true), source: tID }), tShape({ value: tBool(false), source: t.Nil }), ]); export type ThreadPermissionsBlob = { +[permission: string]: ThreadPermissionInfo, }; export type ThreadRolePermissionsBlob = { +[permission: string]: boolean }; export const threadRolePermissionsBlobValidator: TDict = t.dict(t.String, t.Boolean); export type ThreadPermissionsInfo = { +[permission: ThreadPermission]: ThreadPermissionInfo, }; export const threadPermissionsInfoValidator: TDict = t.dict(threadPermissionValidator, threadPermissionInfoValidator); diff --git a/lib/types/validation.test.js b/lib/types/validation.test.js index de97c98c7..7cd176e17 100644 --- a/lib/types/validation.test.js +++ b/lib/types/validation.test.js @@ -1,904 +1,892 @@ // @flow import _findKey from 'lodash/fp/findKey.js'; import { rawEntryInfoValidator } from './entry-types.js'; import { imageValidator, videoValidator, mediaValidator, } from './media-types.js'; import { messageTypes } from './message-types-enum.js'; import { activityUpdateResponseServerSocketMessageValidator, apiResponseServerSocketMessageValidator, authErrorServerSocketMessageValidator, errorServerSocketMessageValidator, messagesServerSocketMessageValidator, pongServerSocketMessageValidator, serverRequestsServerSocketMessageValidator, serverSocketMessageTypes, serverStateSyncServerSocketMessageValidator, serverUpdatesServerSocketMessageValidator, } from './socket-types.js'; import { threadTypes } from './thread-types-enum.js'; import { rawThreadInfoValidator } from './thread-types.js'; import { updateTypes } from './update-types-enum.js'; import { accountDeletionUpdateInfoValidator, badDeviceTokenUpdateInfoValidator, entryUpdateInfoValidator, serverCurrentUserUpdateInfoValidator, threadDeletionUpdateInfoValidator, threadJoinUpdateInfoValidator, threadReadStatusUpdateInfoValidator, threadUpdateInfoValidator, userUpdateInfoValidator, } from './update-types.js'; import { messageSpecs } from '../shared/messages/message-specs.js'; describe('media validation', () => { const photo = { id: '92696', type: 'photo', uri: 'http://0.0.0.0:3000/comm/upload/92696/0fb272bd1c75d976', dimensions: { width: 340, height: 288, }, }; const video = { type: 'video', id: '92769', uri: 'http://0.0.0.0:3000/comm/upload/92769/4bcc6987b25b2f66', dimensions: { width: 480, height: 270, }, thumbnailID: '92770', thumbnailURI: 'http://0.0.0.0:3000/comm/upload/92770/d56466051dcef1db', }; it('should validate correct media', () => { expect(mediaValidator.is(photo)).toBe(true); expect(imageValidator.is(photo)).toBe(true); expect(mediaValidator.is(video)).toBe(true); expect(videoValidator.is(video)).toBe(true); }); it('should not validate incorrect media', () => { expect(imageValidator.is(video)).toBe(false); expect(videoValidator.is(photo)).toBe(false); expect(mediaValidator.is({ ...photo, type: undefined })).toBe(false); expect(mediaValidator.is({ ...video, dimensions: undefined })).toBe(false); }); }); const messages = [ { type: messageTypes.TEXT, threadID: '83859', creatorID: '83853', time: 1682077048858, text: 'text', localID: 'local1', id: '92837', }, { type: messageTypes.CREATE_THREAD, id: '83876', threadID: '83859', time: 1673561105839, creatorID: '83853', initialThreadState: { type: 6, name: null, parentThreadID: '1', color: '57697f', memberIDs: ['256', '83853'], }, }, { type: messageTypes.ADD_MEMBERS, id: '4754380', threadID: '4746046', time: 1680179819346, creatorID: '256', addedUserIDs: ['518252', '1329299', '1559042'], }, { type: messageTypes.CREATE_SUB_THREAD, threadID: '87111', creatorID: '83928', time: 1682083573756, childThreadID: '92993', id: '93000', }, { type: messageTypes.CHANGE_SETTINGS, threadID: '83859', creatorID: '83853', time: 1682082984605, field: 'color', value: 'b8753d', id: '92880', }, { type: messageTypes.REMOVE_MEMBERS, threadID: '92993', creatorID: '83928', time: 1682083613415, removedUserIDs: ['83890'], id: '93012', }, { type: messageTypes.CHANGE_ROLE, threadID: '85027', creatorID: '256', time: 1632393331694, userIDs: ['85081'], newRole: 'role', id: '85431', }, { type: messageTypes.LEAVE_THREAD, id: '93027', threadID: '92993', time: 1682083651037, creatorID: '83928', }, { type: messageTypes.JOIN_THREAD, threadID: '92993', creatorID: '83928', time: 1682083678595, id: '93035', }, { type: messageTypes.CREATE_ENTRY, threadID: '84695', creatorID: '83928', time: 1682083217395, entryID: '92917', date: '2023-04-02', text: 'text', id: '92920', }, { type: messageTypes.EDIT_ENTRY, threadID: '84695', creatorID: '83928', time: 1682083374471, entryID: '92917', date: '2023-04-02', text: 'text', id: '92950', }, { type: messageTypes.DELETE_ENTRY, threadID: '86033', creatorID: '83928', time: 1682083220296, entryID: '92904', date: '2023-04-02', text: 'text', id: '92932', }, { type: messageTypes.RESTORE_ENTRY, id: '92962', threadID: '86033', time: 1682083414244, creatorID: '83928', entryID: '92904', date: '2023-04-02', text: 'text', }, { type: messageTypes.UNSUPPORTED, threadID: '87080', creatorID: '256', time: 1640733462322, robotext: 'unsupported message', unsupportedMessageInfo: { type: 105, threadID: '97489', creatorID: '256', time: 1640773011289, id: '97672', }, id: '97730', }, { type: messageTypes.IMAGES, threadID: '92796', creatorID: '83928', time: 1682083469079, media: [ { id: '92974', uri: 'http://0.0.0.0:3000/comm/upload/92974/ff3d02ded71e2762', type: 'photo', dimensions: { width: 220, height: 220, }, }, ], localID: 'local0', id: '92976', }, { type: messageTypes.MULTIMEDIA, threadID: '89644', creatorID: '83853', time: 1682076177257, media: [ { type: 'video', id: '92769', uri: 'http://0.0.0.0:3000/comm/upload/92769/4bcc6987b25b2f66', dimensions: { width: 480, height: 270, }, thumbnailID: '92770', thumbnailURI: 'http://0.0.0.0:3000/comm/upload/92770/d56466051dcef1db', }, ], id: '92771', }, { type: messageTypes.UPDATE_RELATIONSHIP, threadID: '92796', creatorID: '83928', targetID: '83853', time: 1682083716312, operation: 'request_sent', id: '93039', }, { type: messageTypes.SIDEBAR_SOURCE, threadID: '93044', creatorID: '83928', time: 1682083756831, sourceMessage: { type: 0, id: '92816', threadID: '92796', time: 1682076737518, creatorID: '83928', text: 'text', }, id: '93049', }, { type: messageTypes.CREATE_SIDEBAR, threadID: '93044', creatorID: '83928', time: 1682083756831, sourceMessageAuthorID: '83928', initialThreadState: { name: 'text', parentThreadID: '92796', color: 'aa4b4b', memberIDs: ['83853', '83928'], }, id: '93050', }, { type: messageTypes.REACTION, threadID: '86033', localID: 'local8', creatorID: '83928', time: 1682083295820, targetMessageID: '91607', reaction: '😂', action: 'add_reaction', id: '92943', }, { type: messageTypes.EDIT_MESSAGE, threadID: '86033', creatorID: '83928', time: 1682083295820, targetMessageID: '91607', text: 'text', id: '92943', }, { type: messageTypes.TOGGLE_PIN, threadID: '86033', targetMessageID: '91607', action: 'pin', pinnedContent: 'text', creatorID: '83928', time: 1682083295820, id: '92943', }, ]; describe('message validation', () => { for (const validatorMessageTypeName in messageTypes) { const validatorMessageType = messageTypes[validatorMessageTypeName]; const validator = messageSpecs[validatorMessageType].validator; for (const message of messages) { const messageTypeName = _findKey(e => e === message.type)(messageTypes); if (validatorMessageType === message.type) { it(`${validatorMessageTypeName} should validate ${messageTypeName}`, () => { expect(validator.is(message)).toBe(true); }); } else if ( !( (validatorMessageType === messageTypes.IMAGES && message.type === messageTypes.MULTIMEDIA) || (validatorMessageType === messageTypes.MULTIMEDIA && message.type === messageTypes.IMAGES) ) ) { it(`${validatorMessageTypeName} shouldn't validate ${messageTypeName}`, () => { expect(validator.is(message)).toBe(false); }); } } } }); const thread = { id: '85171', type: threadTypes.PERSONAL, name: '', description: '', color: '6d49ab', creationTime: 1675887298557, parentThreadID: '1', members: [ { id: '256', role: null, permissions: { know_of: { value: true, source: '1', }, - membership: { - value: false, - source: null, - }, 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', }, }, isSender: false, }, { id: '83853', role: '85172', permissions: { know_of: { value: true, source: '85171', }, - membership: { - value: false, - source: null, - }, 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, }, }, 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', }, - membership: { - value: false, - source: null, - }, 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, }, }, subscription: { home: true, pushNotifs: true, }, unread: false, }, repliesCount: 0, containingThreadID: '1', community: '1', pinnedCount: 0, }; describe('thread validation', () => { it('should validate correct thread', () => { expect(rawThreadInfoValidator.is(thread)).toBe(true); }); it('should not validate incorrect thread', () => { expect( rawThreadInfoValidator.is({ ...thread, creationTime: undefined }), ).toBe(false); }); }); const entry = { id: '92860', threadID: '85068', text: 'text', year: 2023, month: 4, day: 2, creationTime: 1682082939882, creatorID: '83853', deleted: false, }; describe('entry validation', () => { it('should validate correct entry', () => { expect(rawEntryInfoValidator.is(entry)).toBe(true); }); it('should not validate incorrect entry', () => { expect(rawEntryInfoValidator.is({ ...entry, threadID: 0 })).toBe(false); }); }); const updates = [ { type: updateTypes.DELETE_ACCOUNT, id: '98424', time: 1640870111106, deletedUserID: '98262', }, { type: updateTypes.UPDATE_THREAD, id: '97948', time: 1640868525494, threadInfo: thread, }, { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: '98002', time: 1640869373326, threadID: '83794', unread: true, }, { type: updateTypes.DELETE_THREAD, id: '98208', time: 1640869773339, threadID: '97852', }, { type: updateTypes.JOIN_THREAD, id: '98126', time: 1640869494461, threadInfo: thread, rawMessageInfos: messages, truncationStatus: 'exhaustive', rawEntryInfos: [entry], }, { type: updateTypes.BAD_DEVICE_TOKEN, id: '98208', time: 1640869773495, deviceToken: 'some-device-token', }, { type: updateTypes.UPDATE_ENTRY, id: '98233', time: 1640869844908, entryInfo: entry, }, { type: updateTypes.UPDATE_CURRENT_USER, id: '98237', time: 1640869934058, currentUserInfo: { id: '256', username: 'ashoat', }, }, { type: updateTypes.UPDATE_USER, id: '97988', time: 1640869211822, updatedUserID: '86565', }, ]; describe('server update validation', () => { const validatorByUpdateType = { [updateTypes.DELETE_ACCOUNT]: accountDeletionUpdateInfoValidator, [updateTypes.UPDATE_THREAD]: threadUpdateInfoValidator, [updateTypes.UPDATE_THREAD_READ_STATUS]: threadReadStatusUpdateInfoValidator, [updateTypes.DELETE_THREAD]: threadDeletionUpdateInfoValidator, [updateTypes.JOIN_THREAD]: threadJoinUpdateInfoValidator, [updateTypes.BAD_DEVICE_TOKEN]: badDeviceTokenUpdateInfoValidator, [updateTypes.UPDATE_ENTRY]: entryUpdateInfoValidator, [updateTypes.UPDATE_CURRENT_USER]: serverCurrentUserUpdateInfoValidator, [updateTypes.UPDATE_USER]: userUpdateInfoValidator, }; for (const validatorUpdateType in validatorByUpdateType) { const validator = validatorByUpdateType[validatorUpdateType]; const validatorUpdateTypeName = _findKey( e => e === Number(validatorUpdateType), )(updateTypes); for (const update of updates) { const updateTypeName = _findKey(e => e === update.type)(updateTypes); if (Number(validatorUpdateType) === update.type) { it(`${validatorUpdateTypeName} should validate ${updateTypeName}`, () => { expect(validator.is(update)).toBe(true); }); } else { it(`${validatorUpdateTypeName} shouldn't validate ${updateTypeName}`, () => { expect(validator.is(update)).toBe(false); }); } } } }); describe('socket message validation', () => { const socketMessages = [ { type: serverSocketMessageTypes.STATE_SYNC, responseTo: 0, payload: { type: 1, messagesResult: { rawMessageInfos: messages, truncationStatuses: { '86033': 'unchanged' }, currentAsOf: 1683296863468, }, updatesResult: { newUpdates: updates, currentAsOf: 1683296863489, }, deltaEntryInfos: [], deletedEntryIDs: [], userInfos: [], }, }, { type: serverSocketMessageTypes.REQUESTS, payload: { serverRequests: [ { type: 6, hashesToCheck: { threadInfos: 3311950643, entryInfos: 3191324567, currentUserInfo: 820850779, userInfos: 707653884, }, }, ], }, }, { type: serverSocketMessageTypes.ACTIVITY_UPDATE_RESPONSE, responseTo: 194, payload: { unfocusedToUnread: [] }, }, { type: serverSocketMessageTypes.PONG, responseTo: 190 }, { type: 6, payload: { updatesResult: { currentAsOf: 1683298141720, newUpdates: [ { type: 1, id: '94428', time: 1683298141720, threadInfo: thread, }, ], }, userInfos: [], }, }, { type: serverSocketMessageTypes.MESSAGES, payload: { messagesResult: { rawMessageInfos: messages, truncationStatuses: { '86033': 'unchanged' }, currentAsOf: 1683298141707, }, }, }, { type: serverSocketMessageTypes.API_RESPONSE, responseTo: 209, payload: { rawMessageInfos: messages, truncationStatuses: { '1': 'exhaustive' }, userInfos: {}, }, }, ]; const validatorByMessageType = { [serverSocketMessageTypes.STATE_SYNC]: serverStateSyncServerSocketMessageValidator, [serverSocketMessageTypes.REQUESTS]: serverRequestsServerSocketMessageValidator, [serverSocketMessageTypes.ERROR]: errorServerSocketMessageValidator, [serverSocketMessageTypes.AUTH_ERROR]: authErrorServerSocketMessageValidator, [serverSocketMessageTypes.ACTIVITY_UPDATE_RESPONSE]: activityUpdateResponseServerSocketMessageValidator, [serverSocketMessageTypes.PONG]: pongServerSocketMessageValidator, [serverSocketMessageTypes.UPDATES]: serverUpdatesServerSocketMessageValidator, [serverSocketMessageTypes.MESSAGES]: messagesServerSocketMessageValidator, [serverSocketMessageTypes.API_RESPONSE]: apiResponseServerSocketMessageValidator, }; for (const validatorMessageType in validatorByMessageType) { const validator = validatorByMessageType[validatorMessageType]; const validatorMessageTypeName = _findKey( e => e === Number(validatorMessageType), )(serverSocketMessageTypes); for (const message of socketMessages) { const messageTypeName = _findKey(e => e === message.type)( serverSocketMessageTypes, ); if (Number(validatorMessageType) === message.type) { it(`${validatorMessageTypeName} should validate ${messageTypeName}`, () => { expect(validator.is(message)).toBe(true); }); } else { it(`${validatorMessageTypeName} shouldn't validate ${messageTypeName}`, () => { expect(validator.is(message)).toBe(false); }); } } } }); diff --git a/lib/utils/thread-ops-utils.test.js b/lib/utils/thread-ops-utils.test.js index 5fb94782f..0c1e74598 100644 --- a/lib/utils/thread-ops-utils.test.js +++ b/lib/utils/thread-ops-utils.test.js @@ -1,445 +1,429 @@ // @flow import { convertClientDBThreadInfoToRawThreadInfo, convertRawThreadInfoToClientDBThreadInfo, } from './thread-ops-utils.js'; import type { ClientDBThreadInfo, RawThreadInfo, } from '../types/thread-types.js'; const rawThreadInfo: RawThreadInfo = { id: '84015', type: 6, name: 'atul_web', description: 'Hello world!', color: '4b87aa', creationTime: 1679595843051, parentThreadID: '1', members: [ { id: '256', role: null, permissions: { know_of: { value: true, source: '1', }, - membership: { - value: false, - source: null, - }, 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, }, }, isSender: false, }, { id: '83809', role: '84016', permissions: { know_of: { value: true, source: '84015', }, - membership: { - value: false, - source: null, - }, visible: { value: true, source: '84015', }, voiced: { value: true, source: '84015', }, edit_entries: { value: true, source: '84015', }, edit_thread: { value: true, source: '84015', }, edit_thread_description: { value: true, source: '84015', }, edit_thread_color: { value: true, source: '84015', }, delete_thread: { value: false, source: null, }, create_subthreads: { value: false, source: null, }, create_sidebars: { value: true, source: '84015', }, 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: '84015', }, edit_message: { value: true, source: '84015', }, }, isSender: true, }, { id: '83969', role: '84016', permissions: { know_of: { value: true, source: '84015', }, - membership: { - value: false, - source: null, - }, visible: { value: true, source: '84015', }, voiced: { value: true, source: '84015', }, edit_entries: { value: true, source: '84015', }, edit_thread: { value: true, source: '84015', }, edit_thread_description: { value: true, source: '84015', }, edit_thread_color: { value: true, source: '84015', }, delete_thread: { value: false, source: null, }, create_subthreads: { value: false, source: null, }, create_sidebars: { value: true, source: '84015', }, 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: '84015', }, edit_message: { value: true, source: '84015', }, }, isSender: true, }, ], roles: { '84016': { id: '84016', 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: '84016', permissions: { know_of: { value: true, source: '84015', }, - membership: { - value: false, - source: null, - }, visible: { value: true, source: '84015', }, voiced: { value: true, source: '84015', }, edit_entries: { value: true, source: '84015', }, edit_thread: { value: true, source: '84015', }, edit_thread_description: { value: true, source: '84015', }, edit_thread_color: { value: true, source: '84015', }, delete_thread: { value: false, source: null, }, create_subthreads: { value: false, source: null, }, create_sidebars: { value: true, source: '84015', }, 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: '84015', }, edit_message: { value: true, source: '84015', }, }, subscription: { home: true, pushNotifs: true, }, unread: false, }, repliesCount: 0, containingThreadID: '1', community: '1', pinnedCount: 0, }; const clientDBThreadInfo: ClientDBThreadInfo = { id: '84015', type: 6, name: 'atul_web', description: 'Hello world!', color: '4b87aa', creationTime: '1679595843051', parentThreadID: '1', members: - '[{"id":"256","role":null,"permissions":{"know_of":{"value":true,"source":"1"},"membership":{"value":false,"source":null},"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}},"isSender":false},{"id":"83809","role":"84016","permissions":{"know_of":{"value":true,"source":"84015"},"membership":{"value":false,"source":null},"visible":{"value":true,"source":"84015"},"voiced":{"value":true,"source":"84015"},"edit_entries":{"value":true,"source":"84015"},"edit_thread":{"value":true,"source":"84015"},"edit_thread_description":{"value":true,"source":"84015"},"edit_thread_color":{"value":true,"source":"84015"},"delete_thread":{"value":false,"source":null},"create_subthreads":{"value":false,"source":null},"create_sidebars":{"value":true,"source":"84015"},"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":"84015"},"edit_message":{"value":true,"source":"84015"}},"isSender":true},{"id":"83969","role":"84016","permissions":{"know_of":{"value":true,"source":"84015"},"membership":{"value":false,"source":null},"visible":{"value":true,"source":"84015"},"voiced":{"value":true,"source":"84015"},"edit_entries":{"value":true,"source":"84015"},"edit_thread":{"value":true,"source":"84015"},"edit_thread_description":{"value":true,"source":"84015"},"edit_thread_color":{"value":true,"source":"84015"},"delete_thread":{"value":false,"source":null},"create_subthreads":{"value":false,"source":null},"create_sidebars":{"value":true,"source":"84015"},"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":"84015"},"edit_message":{"value":true,"source":"84015"}},"isSender":true}]', + '[{"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}},"isSender":false},{"id":"83809","role":"84016","permissions":{"know_of":{"value":true,"source":"84015"},"visible":{"value":true,"source":"84015"},"voiced":{"value":true,"source":"84015"},"edit_entries":{"value":true,"source":"84015"},"edit_thread":{"value":true,"source":"84015"},"edit_thread_description":{"value":true,"source":"84015"},"edit_thread_color":{"value":true,"source":"84015"},"delete_thread":{"value":false,"source":null},"create_subthreads":{"value":false,"source":null},"create_sidebars":{"value":true,"source":"84015"},"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":"84015"},"edit_message":{"value":true,"source":"84015"}},"isSender":true},{"id":"83969","role":"84016","permissions":{"know_of":{"value":true,"source":"84015"},"visible":{"value":true,"source":"84015"},"voiced":{"value":true,"source":"84015"},"edit_entries":{"value":true,"source":"84015"},"edit_thread":{"value":true,"source":"84015"},"edit_thread_description":{"value":true,"source":"84015"},"edit_thread_color":{"value":true,"source":"84015"},"delete_thread":{"value":false,"source":null},"create_subthreads":{"value":false,"source":null},"create_sidebars":{"value":true,"source":"84015"},"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":"84015"},"edit_message":{"value":true,"source":"84015"}},"isSender":true}]', roles: '{"84016":{"id":"84016","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":"84016","permissions":{"know_of":{"value":true,"source":"84015"},"membership":{"value":false,"source":null},"visible":{"value":true,"source":"84015"},"voiced":{"value":true,"source":"84015"},"edit_entries":{"value":true,"source":"84015"},"edit_thread":{"value":true,"source":"84015"},"edit_thread_description":{"value":true,"source":"84015"},"edit_thread_color":{"value":true,"source":"84015"},"delete_thread":{"value":false,"source":null},"create_subthreads":{"value":false,"source":null},"create_sidebars":{"value":true,"source":"84015"},"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":"84015"},"edit_message":{"value":true,"source":"84015"}},"subscription":{"home":true,"pushNotifs":true},"unread":false}', + '{"role":"84016","permissions":{"know_of":{"value":true,"source":"84015"},"visible":{"value":true,"source":"84015"},"voiced":{"value":true,"source":"84015"},"edit_entries":{"value":true,"source":"84015"},"edit_thread":{"value":true,"source":"84015"},"edit_thread_description":{"value":true,"source":"84015"},"edit_thread_color":{"value":true,"source":"84015"},"delete_thread":{"value":false,"source":null},"create_subthreads":{"value":false,"source":null},"create_sidebars":{"value":true,"source":"84015"},"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":"84015"},"edit_message":{"value":true,"source":"84015"}},"subscription":{"home":true,"pushNotifs":true},"unread":false}', repliesCount: 0, containingThreadID: '1', community: '1', avatar: null, pinnedCount: 0, }; describe('convertRawThreadInfoToClientDBThreadInfo', () => { it('should successfuly convert RawThreadInfo to ClientDBThreadInfo', () => { expect( convertRawThreadInfoToClientDBThreadInfo(rawThreadInfo), ).toStrictEqual(clientDBThreadInfo); }); }); describe('convertClientDBThreadInfoToRawThreadInfo', () => { it('should successfuly convert ClientDBThreadInfo to RawThreadInfo', () => { expect( convertClientDBThreadInfoToRawThreadInfo( convertRawThreadInfoToClientDBThreadInfo(rawThreadInfo), ), ).toStrictEqual(rawThreadInfo); }); }); const rawThreadInfoWithAvatar: RawThreadInfo = { ...rawThreadInfo, avatar: { type: 'emoji', color: '4b87aa', emoji: '😀' }, }; describe('convertRawThreadInfoToClientDBThreadInfo', () => { it('should NOT convert rawThreadInfoWithAvatar to clientDBThreadInfo (without avatar)', () => { expect( convertRawThreadInfoToClientDBThreadInfo(rawThreadInfoWithAvatar), ).not.toStrictEqual(clientDBThreadInfo); }); }); describe('convertClientDBThreadInfoToRawThreadInfo', () => { it('should successfuly recreate original rawThreadInfoWithAvatar', () => { expect( convertClientDBThreadInfoToRawThreadInfo( convertRawThreadInfoToClientDBThreadInfo(rawThreadInfoWithAvatar), ), ).toStrictEqual(rawThreadInfoWithAvatar); }); });