diff --git a/lib/shared/state-sync/entries-state-sync-spec.js b/lib/shared/state-sync/entries-state-sync-spec.js index 409967b7e..bee6173dd 100644 --- a/lib/shared/state-sync/entries-state-sync-spec.js +++ b/lib/shared/state-sync/entries-state-sync-spec.js @@ -1,118 +1,127 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import { createSelector } from 'reselect'; import type { StateSyncSpec, BoundStateSyncSpec } from './state-sync-spec.js'; +import { extractKeyserverIDFromID } from '../../keyserver-conn/keyserver-call-utils.js'; import { type CalendarQuery, type RawEntryInfos, type RawEntryInfo, } from '../../types/entry-types.js'; import type { AppState } from '../../types/redux-types.js'; import { reportTypes, type ClientEntryInconsistencyReportCreationRequest, } from '../../types/report-types.js'; import type { ProcessServerRequestAction } from '../../types/request-types.js'; import { actionLogger } from '../../utils/action-logger.js'; import { authoritativeKeyserverID } from '../../utils/authoritative-keyserver.js'; import { getConfig } from '../../utils/config.js'; import { values, combineUnorderedHashes, hash } from '../../utils/objects.js'; import { generateReportID } from '../../utils/report-utils.js'; import { sanitizeActionSecrets } from '../../utils/sanitization.js'; import { filterRawEntryInfosByCalendarQuery, serverEntryInfosObject, } from '../entry-utils.js'; const selector: ( state: AppState, ) => BoundStateSyncSpec< RawEntryInfos, RawEntryInfo, $ReadOnlyArray, > = createSelector( (state: AppState) => state.entryStore.entryInfos, (entryInfos: RawEntryInfos) => ({ ...entriesStateSyncSpec, getInfoHash: (id: string) => hash(entryInfos[`${authoritativeKeyserverID()}|${id}`]), getAllInfosHash: (calendarQuery: CalendarQuery) => getEntryInfosHash(entryInfos, calendarQuery), getIDs: (calendarQuery: CalendarQuery) => getEntryIDs(entryInfos, calendarQuery), }), ); export const entriesStateSyncSpec: StateSyncSpec< RawEntryInfos, RawEntryInfo, $ReadOnlyArray, > = Object.freeze({ hashKey: 'entryInfos', innerHashSpec: { hashKey: 'entryInfo', deleteKey: 'deleteEntryIDs', rawInfosKey: 'rawEntryInfos', }, findStoreInconsistencies( action: ProcessServerRequestAction, beforeStateCheck: RawEntryInfos, afterStateCheck: RawEntryInfos, ) { + const keyserverID = action.payload.keyserverID; + const filteredBeforeStateCheck = values(beforeStateCheck).filter( + entry => entry.id && extractKeyserverIDFromID(entry.id) === keyserverID, + ); + const filteredAfterStateCheck = values(afterStateCheck).filter( + entry => entry.id && extractKeyserverIDFromID(entry.id) === keyserverID, + ); + const calendarQuery = action.payload.calendarQuery; // We don't want to bother reporting an inconsistency if it's just because // of extraneous EntryInfos (not within the current calendarQuery) on either // side const filteredBeforeResult = filterRawEntryInfosByCalendarQuery( - serverEntryInfosObject(values(beforeStateCheck)), + serverEntryInfosObject(filteredBeforeStateCheck), calendarQuery, ); const filteredAfterResult = filterRawEntryInfosByCalendarQuery( - serverEntryInfosObject(values(afterStateCheck)), + serverEntryInfosObject(filteredAfterStateCheck), calendarQuery, ); if (_isEqual(filteredBeforeResult)(filteredAfterResult)) { return emptyArray; } return [ { type: reportTypes.ENTRY_INCONSISTENCY, platformDetails: getConfig().platformDetails, beforeAction: beforeStateCheck, action: sanitizeActionSecrets(action), calendarQuery, pushResult: afterStateCheck, lastActions: actionLogger.interestingActionSummaries, time: Date.now(), id: generateReportID(), }, ]; }, selector, }); const emptyArray: $ReadOnlyArray = []; function getEntryInfosHash( entryInfos: RawEntryInfos, calendarQuery: CalendarQuery, ) { const filteredEntryInfos = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(values(entryInfos)), calendarQuery, ); return combineUnorderedHashes(Object.values(filteredEntryInfos).map(hash)); } function getEntryIDs(entryInfos: RawEntryInfos, calendarQuery: CalendarQuery) { const filteredEntryInfos = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(values(entryInfos)), calendarQuery, ); return Object.keys(filteredEntryInfos).map(id => id.split('|')[1]); } diff --git a/lib/shared/state-sync/entries-state-sync-spec.test.js b/lib/shared/state-sync/entries-state-sync-spec.test.js new file mode 100644 index 000000000..4e747852c --- /dev/null +++ b/lib/shared/state-sync/entries-state-sync-spec.test.js @@ -0,0 +1,128 @@ +// @flow + +import { entriesStateSyncSpec } from './entries-state-sync-spec.js'; +import { processServerRequestsActionType } from '../../types/request-types.js'; + +jest.mock('../../utils/config.js'); + +describe('Entries state sync spec', () => { + describe('find store inconsistencies', () => { + it('should find inconsistencies from the same keyserver', () => { + const before = { + '256|93372': { + id: '256|93372', + threadID: '256|84459', + text: '123', + year: 2022, + month: 2, + day: 27, + creationTime: 1709035016680, + creatorID: '84447', + deleted: false, + }, + }; + + const after = { + '256|123': { + id: '256|123', + threadID: '256|84459', + text: '123', + year: 2022, + month: 2, + day: 27, + creationTime: 1709035016680, + creatorID: '84447', + deleted: false, + }, + '256|93372': { + id: '256|93372', + threadID: '256|84459', + text: '123', + year: 2022, + month: 2, + day: 27, + creationTime: 1709035016680, + creatorID: '84447', + deleted: false, + }, + }; + + const action = { + type: processServerRequestsActionType, + payload: { + serverRequests: [], + calendarQuery: { + startDate: '2022-01-01', + endDate: '2022-03-01', + filters: [], + }, + keyserverID: '256', + }, + }; + + expect( + entriesStateSyncSpec.findStoreInconsistencies(action, before, after) + .length, + ).toEqual(1); + }); + + it('should ignore inconsistencies from different keyservers', () => { + const before = { + '256|93372': { + id: '256|93372', + threadID: '256|84459', + text: '123', + year: 2022, + month: 2, + day: 27, + creationTime: 1709035016680, + creatorID: '84447', + deleted: false, + }, + }; + + const after = { + '123|123': { + id: '123|123', + threadID: '256|84459', + text: '123', + year: 2022, + month: 2, + day: 27, + creationTime: 1709035016680, + creatorID: '84447', + deleted: false, + }, + '256|93372': { + id: '256|93372', + threadID: '256|84459', + text: '123', + year: 2022, + month: 2, + day: 27, + creationTime: 1709035016680, + creatorID: '84447', + deleted: false, + }, + }; + + const action = { + type: processServerRequestsActionType, + payload: { + serverRequests: [], + calendarQuery: { + startDate: '2022-01-01', + endDate: '2022-03-01', + filters: [], + }, + keyserverID: '256', + }, + }; + + expect( + entriesStateSyncSpec.findStoreInconsistencies(action, before, after) + .length, + ).toEqual(0); + }); + }); +}); diff --git a/lib/shared/state-sync/threads-state-sync-spec.js b/lib/shared/state-sync/threads-state-sync-spec.js index 7b156f353..d1d175871 100644 --- a/lib/shared/state-sync/threads-state-sync-spec.js +++ b/lib/shared/state-sync/threads-state-sync-spec.js @@ -1,83 +1,91 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; +import _pickBy from 'lodash/fp/pickBy.js'; import { createSelector } from 'reselect'; import type { StateSyncSpec, BoundStateSyncSpec } from './state-sync-spec.js'; +import { extractKeyserverIDFromID } from '../../keyserver-conn/keyserver-call-utils.js'; import type { RawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { AppState } from '../../types/redux-types.js'; import { reportTypes, type ClientThreadInconsistencyReportCreationRequest, } from '../../types/report-types.js'; import type { ProcessServerRequestAction } from '../../types/request-types.js'; import { type MixedRawThreadInfos, type LegacyRawThreadInfo, } from '../../types/thread-types.js'; import { actionLogger } from '../../utils/action-logger.js'; import { authoritativeKeyserverID } from '../../utils/authoritative-keyserver.js'; import { getConfig } from '../../utils/config.js'; import { combineUnorderedHashes, values } from '../../utils/objects.js'; import { generateReportID } from '../../utils/report-utils.js'; import { sanitizeActionSecrets } from '../../utils/sanitization.js'; const selector: ( state: AppState, ) => BoundStateSyncSpec< MixedRawThreadInfos, LegacyRawThreadInfo | RawThreadInfo, $ReadOnlyArray, > = createSelector( (state: AppState) => state.integrityStore.threadHashes, (state: AppState) => state.integrityStore.threadHashingStatus === 'completed', (threadHashes: { +[string]: number }, threadHashingComplete: boolean) => ({ ...threadsStateSyncSpec, getInfoHash: (id: string) => threadHashes[`${authoritativeKeyserverID()}|${id}`], getAllInfosHash: threadHashingComplete ? () => combineUnorderedHashes(values(threadHashes)) : () => null, getIDs: threadHashingComplete ? () => Object.keys(threadHashes).map(id => id.split('|')[1]) : () => null, }), ); export const threadsStateSyncSpec: StateSyncSpec< MixedRawThreadInfos, LegacyRawThreadInfo | RawThreadInfo, $ReadOnlyArray, > = Object.freeze({ hashKey: 'threadInfos', innerHashSpec: { hashKey: 'threadInfo', deleteKey: 'deleteThreadIDs', rawInfosKey: 'rawThreadInfos', }, findStoreInconsistencies( action: ProcessServerRequestAction, beforeStateCheck: MixedRawThreadInfos, afterStateCheck: MixedRawThreadInfos, ) { - if (_isEqual(beforeStateCheck)(afterStateCheck)) { + const keyserverID = action.payload.keyserverID; + const filter = _pickBy( + thread => extractKeyserverIDFromID(thread.id) === keyserverID, + ); + const filteredBeforeStateCheck = filter(beforeStateCheck); + const filteredAfterStateCheck = filter(afterStateCheck); + if (_isEqual(filteredBeforeStateCheck)(filteredAfterStateCheck)) { return emptyArray; } return [ { type: reportTypes.THREAD_INCONSISTENCY, platformDetails: getConfig().platformDetails, beforeAction: beforeStateCheck, action: sanitizeActionSecrets(action), pushResult: afterStateCheck, lastActions: actionLogger.interestingActionSummaries, time: Date.now(), id: generateReportID(), }, ]; }, selector, }); const emptyArray: $ReadOnlyArray = []; diff --git a/lib/shared/state-sync/threads-state-sync-spec.test.js b/lib/shared/state-sync/threads-state-sync-spec.test.js new file mode 100644 index 000000000..c8c9b70c1 --- /dev/null +++ b/lib/shared/state-sync/threads-state-sync-spec.test.js @@ -0,0 +1,217 @@ +// @flow + +import { threadsStateSyncSpec } from './threads-state-sync-spec.js'; +import { defaultCalendarQuery } from '../../types/entry-types.js'; +import { processServerRequestsActionType } from '../../types/request-types.js'; +import { threadTypes } from '../../types/thread-types-enum.js'; +import { getConfig } from '../../utils/config.js'; + +jest.mock('../../utils/config.js'); + +describe('Threads state sync spec', () => { + describe('find store inconsistencies', () => { + it('should find inconsistencies from the same keyserver', () => { + const before = { + '256|84459': { + minimallyEncoded: true, + id: '256|84459', + type: threadTypes.PRIVATE, + name: '', + description: '', + color: 'b8753d', + creationTime: 3663, + parentThreadID: '256|1', + containingThreadID: '256|1', + community: '256|1', + members: [], + roles: {}, + currentUser: { + role: '256|84460', + permissions: '3026f', + subscription: { + home: true, + pushNotifs: true, + }, + unread: false, + minimallyEncoded: true, + }, + repliesCount: 0, + pinnedCount: 0, + }, + }; + + const after = { + '256|84459': { + minimallyEncoded: true, + id: '256|84459', + type: threadTypes.PRIVATE, + name: '', + description: '', + color: 'b8753d', + creationTime: 3663, + parentThreadID: '256|1', + containingThreadID: '256|1', + community: '256|1', + members: [], + roles: {}, + currentUser: { + role: '256|84460', + permissions: '3026f', + subscription: { + home: true, + pushNotifs: true, + }, + unread: false, + minimallyEncoded: true, + }, + repliesCount: 0, + pinnedCount: 0, + }, + '256|123': { + minimallyEncoded: true, + id: '256|123', + type: threadTypes.PRIVATE, + name: '', + description: '', + color: 'b8753d', + creationTime: 3663, + parentThreadID: '256|1', + containingThreadID: '256|1', + community: '256|1', + members: [], + roles: {}, + currentUser: { + role: '256|84460', + permissions: '3026f', + subscription: { + home: true, + pushNotifs: true, + }, + unread: false, + minimallyEncoded: true, + }, + repliesCount: 0, + pinnedCount: 0, + }, + }; + + const action = { + type: processServerRequestsActionType, + payload: { + serverRequests: [], + calendarQuery: defaultCalendarQuery( + getConfig().platformDetails.platform, + ), + keyserverID: '256', + }, + }; + + expect( + threadsStateSyncSpec.findStoreInconsistencies(action, before, after) + .length, + ).toEqual(1); + }); + + it('should ignore inconsistencies from different keyservers', () => { + const before = { + '256|84459': { + minimallyEncoded: true, + id: '256|84459', + type: threadTypes.PRIVATE, + name: '', + description: '', + color: 'b8753d', + creationTime: 3663, + parentThreadID: '256|1', + containingThreadID: '256|1', + community: '256|1', + members: [], + roles: {}, + currentUser: { + role: '256|84460', + permissions: '3026f', + subscription: { + home: true, + pushNotifs: true, + }, + unread: false, + minimallyEncoded: true, + }, + repliesCount: 0, + pinnedCount: 0, + }, + }; + + const after = { + '256|84459': { + minimallyEncoded: true, + id: '256|84459', + type: threadTypes.PRIVATE, + name: '', + description: '', + color: 'b8753d', + creationTime: 3663, + parentThreadID: '256|1', + containingThreadID: '256|1', + community: '256|1', + members: [], + roles: {}, + currentUser: { + role: '256|84460', + permissions: '3026f', + subscription: { + home: true, + pushNotifs: true, + }, + unread: false, + minimallyEncoded: true, + }, + repliesCount: 0, + pinnedCount: 0, + }, + '123|123': { + minimallyEncoded: true, + id: '123|123', + type: threadTypes.PRIVATE, + name: '', + description: '', + color: 'b8753d', + creationTime: 3663, + parentThreadID: '256|1', + containingThreadID: '256|1', + community: '256|1', + members: [], + roles: {}, + currentUser: { + role: '256|84460', + permissions: '3026f', + subscription: { + home: true, + pushNotifs: true, + }, + unread: false, + minimallyEncoded: true, + }, + repliesCount: 0, + pinnedCount: 0, + }, + }; + + const action = { + type: processServerRequestsActionType, + payload: { + serverRequests: [], + calendarQuery: defaultCalendarQuery( + getConfig().platformDetails.platform, + ), + keyserverID: '256', + }, + }; + + expect( + threadsStateSyncSpec.findStoreInconsistencies(action, before, after) + .length, + ).toEqual(0); + }); + }); +});