diff --git a/keyserver/src/utils/validation-utils.js b/keyserver/src/utils/validation-utils.js index b29d1d048..7844d75d1 100644 --- a/keyserver/src/utils/validation-utils.js +++ b/keyserver/src/utils/validation-utils.js @@ -1,235 +1,237 @@ // @flow import type { TType } from 'tcomb'; import type { PolicyType } from 'lib/facts/policies.js'; import { hasMinCodeVersion, hasMinStateVersion, } from 'lib/shared/version-utils.js'; import { type PlatformDetails } from 'lib/types/device-types.js'; +import { + convertClientIDsToServerIDs, + convertObject, + convertServerIDsToClientIDs, +} from 'lib/utils/conversion-utils.js'; import { ServerError } from 'lib/utils/errors.js'; import { tCookie, tPassword, tPlatform, tPlatformDetails, assertWithValidator, ashoatKeyserverID, - convertClientIDsToServerIDs, - convertObject, - convertServerIDsToClientIDs, } from 'lib/utils/validation-utils.js'; import { fetchNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { verifyClientSupported } from '../session/version.js'; import type { Viewer } from '../session/viewer.js'; async function validateInput( viewer: Viewer, inputValidator: TType, input: mixed, ): Promise { if (!viewer.isSocket) { await checkClientSupported(viewer, inputValidator, input); } const convertedInput = checkInputValidator(inputValidator, input); if ( hasMinStateVersion(viewer.platformDetails, { native: 43, web: 3, }) ) { try { return convertClientIDsToServerIDs( ashoatKeyserverID, inputValidator, convertedInput, ); } catch (err) { throw new ServerError(err.message); } } return convertedInput; } function validateOutput( platformDetails: ?PlatformDetails, outputValidator: TType, data: T, alwaysConvertSchema?: boolean, ): T { if (!outputValidator.is(data)) { console.trace( 'Output validation failed, validator is:', outputValidator.displayName, ); return data; } if ( hasMinStateVersion(platformDetails, { native: 43, web: 3, }) || alwaysConvertSchema ) { return convertServerIDsToClientIDs( ashoatKeyserverID, outputValidator, data, ); } return data; } function checkInputValidator(inputValidator: TType, input: mixed): T { if (inputValidator.is(input)) { return assertWithValidator(input, inputValidator); } const error = new ServerError('invalid_parameters'); error.sanitizedInput = input ? sanitizeInput(inputValidator, input) : null; throw error; } async function checkClientSupported( viewer: Viewer, inputValidator: ?TType, input: T, ) { let platformDetails; if (inputValidator) { platformDetails = findFirstInputMatchingValidator( inputValidator, tPlatformDetails, input, ); } if (!platformDetails && inputValidator) { const platform = findFirstInputMatchingValidator( inputValidator, tPlatform, input, ); if (platform) { platformDetails = { platform }; } } if (!platformDetails) { ({ platformDetails } = viewer); } await verifyClientSupported(viewer, platformDetails); } const redactedString = '********'; const redactedTypes = [tPassword, tCookie]; function sanitizeInput(inputValidator: TType, input: T): T { return convertObject( inputValidator, input, redactedTypes, () => redactedString, ); } function findFirstInputMatchingValidator( wholeInputValidator: *, inputValidatorToMatch: *, input: *, ): any { if (!wholeInputValidator || input === null || input === undefined) { return null; } if ( wholeInputValidator === inputValidatorToMatch && wholeInputValidator.is(input) ) { return input; } if (wholeInputValidator.meta.kind === 'maybe') { return findFirstInputMatchingValidator( wholeInputValidator.meta.type, inputValidatorToMatch, input, ); } if ( wholeInputValidator.meta.kind === 'interface' && typeof input === 'object' ) { for (const key in input) { const value = input[key]; const validator = wholeInputValidator.meta.props[key]; const innerResult = findFirstInputMatchingValidator( validator, inputValidatorToMatch, value, ); if (innerResult) { return innerResult; } } } if (wholeInputValidator.meta.kind === 'union') { for (const validator of wholeInputValidator.meta.types) { if (validator.is(input)) { return findFirstInputMatchingValidator( validator, inputValidatorToMatch, input, ); } } } if (wholeInputValidator.meta.kind === 'list' && Array.isArray(input)) { const validator = wholeInputValidator.meta.type; for (const value of input) { const innerResult = findFirstInputMatchingValidator( validator, inputValidatorToMatch, value, ); if (innerResult) { return innerResult; } } } return null; } async function policiesValidator( viewer: Viewer, policies: $ReadOnlyArray, ) { if (!policies.length) { return; } if (!hasMinCodeVersion(viewer.platformDetails, { native: 181 })) { return; } const notAcknowledgedPolicies = await fetchNotAcknowledgedPolicies( viewer.id, policies, ); if (notAcknowledgedPolicies.length) { throw new ServerError('policies_not_accepted', { notAcknowledgedPolicies, }); } } export { validateInput, validateOutput, checkInputValidator, redactedString, sanitizeInput, findFirstInputMatchingValidator, checkClientSupported, policiesValidator, }; diff --git a/lib/selectors/socket-selectors.js b/lib/selectors/socket-selectors.js index 93fee07a3..f0d19bf0f 100644 --- a/lib/selectors/socket-selectors.js +++ b/lib/selectors/socket-selectors.js @@ -1,258 +1,255 @@ // @flow import { createSelector } from 'reselect'; import t from 'tcomb'; import { updatesCurrentAsOfSelector, currentAsOfSelector, } from './keyserver-selectors.js'; import { currentCalendarQuery } from './nav-selectors.js'; import { serverEntryInfo, serverEntryInfosObject, filterRawEntryInfosByCalendarQuery, } from '../shared/entry-utils.js'; import threadWatcher from '../shared/thread-watcher.js'; import type { SignedIdentityKeysBlob } from '../types/crypto-types.js'; import { type RawEntryInfo, rawEntryInfoValidator, type CalendarQuery, } from '../types/entry-types.js'; import type { AppState } from '../types/redux-types.js'; import type { ClientReportCreationRequest } from '../types/report-types.js'; import { serverRequestTypes, type ClientServerRequest, type ClientClientResponse, } from '../types/request-types.js'; import type { SessionState } from '../types/session-types.js'; import type { OneTimeKeyGenerator } from '../types/socket-types.js'; import { type RawThreadInfo, rawThreadInfoValidator, } from '../types/thread-types.js'; import { type CurrentUserInfo, currentUserInfoValidator, type UserInfos, userInfosValidator, } from '../types/user-types.js'; import { getConfig } from '../utils/config.js'; +import { convertClientIDsToServerIDs } from '../utils/conversion-utils.js'; import { minimumOneTimeKeysRequired } from '../utils/crypto-utils.js'; import { values, hash } from '../utils/objects.js'; -import { - tID, - convertClientIDsToServerIDs, - ashoatKeyserverID, -} from '../utils/validation-utils.js'; +import { tID, ashoatKeyserverID } from '../utils/validation-utils.js'; const queuedReports: ( state: AppState, ) => $ReadOnlyArray = createSelector( (state: AppState) => state.reportStore.queuedReports, ( mainQueuedReports: $ReadOnlyArray, ): $ReadOnlyArray => mainQueuedReports, ); const getClientResponsesSelector: ( state: AppState, ) => ( calendarActive: boolean, oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: ?() => Promise, getInitialNotificationsEncryptedMessage: ?() => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = createSelector( (state: AppState) => state.threadStore.threadInfos, (state: AppState) => state.entryStore.entryInfos, (state: AppState) => state.userStore.userInfos, (state: AppState) => state.currentUserInfo, currentCalendarQuery, ( threadInfos: { +[id: string]: RawThreadInfo }, entryInfos: { +[id: string]: RawEntryInfo }, userInfos: UserInfos, currentUserInfo: ?CurrentUserInfo, calendarQuery: (calendarActive: boolean) => CalendarQuery, ) => { return async ( calendarActive: boolean, oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: ?() => Promise, getInitialNotificationsEncryptedMessage: ?() => Promise, serverRequests: $ReadOnlyArray, ): Promise<$ReadOnlyArray> => { const clientResponses = []; const serverRequestedPlatformDetails = serverRequests.some( request => request.type === serverRequestTypes.PLATFORM_DETAILS, ); for (const serverRequest of serverRequests) { if ( serverRequest.type === serverRequestTypes.PLATFORM && !serverRequestedPlatformDetails ) { clientResponses.push({ type: serverRequestTypes.PLATFORM, platform: getConfig().platformDetails.platform, }); } else if (serverRequest.type === serverRequestTypes.PLATFORM_DETAILS) { clientResponses.push({ type: serverRequestTypes.PLATFORM_DETAILS, platformDetails: getConfig().platformDetails, }); } else if (serverRequest.type === serverRequestTypes.CHECK_STATE) { const filteredEntryInfos = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(values(entryInfos)), calendarQuery(calendarActive), ); const convertedEntryInfos = convertClientIDsToServerIDs( ashoatKeyserverID, t.dict(tID, rawEntryInfoValidator), filteredEntryInfos, ); const convertedThreadInfos = convertClientIDsToServerIDs( ashoatKeyserverID, t.dict(tID, rawThreadInfoValidator), threadInfos, ); const convertedUserInfos = convertClientIDsToServerIDs( ashoatKeyserverID, userInfosValidator, userInfos, ); const convertedCurrentUserInfo = convertClientIDsToServerIDs( ashoatKeyserverID, t.maybe(currentUserInfoValidator), currentUserInfo, ); const hashResults = {}; for (const key in serverRequest.hashesToCheck) { const expectedHashValue = serverRequest.hashesToCheck[key]; let hashValue; if (key === 'threadInfos') { hashValue = hash(convertedThreadInfos); } else if (key === 'entryInfos') { hashValue = hash(convertedEntryInfos); } else if (key === 'userInfos') { hashValue = hash(convertedUserInfos); } else if (key === 'currentUserInfo') { hashValue = hash(convertedCurrentUserInfo); } else if (key.startsWith('threadInfo|')) { const [, threadID] = key.split('|'); hashValue = hash(convertedThreadInfos[threadID]); } else if (key.startsWith('entryInfo|')) { const [, entryID] = key.split('|'); let rawEntryInfo = convertedEntryInfos[entryID]; if (rawEntryInfo) { rawEntryInfo = serverEntryInfo(rawEntryInfo); } hashValue = hash(rawEntryInfo); } else if (key.startsWith('userInfo|')) { const [, userID] = key.split('|'); hashValue = hash(convertedUserInfos[userID]); } else { continue; } hashResults[key] = expectedHashValue === hashValue; } const { failUnmentioned } = serverRequest; if (failUnmentioned && failUnmentioned.threadInfos) { for (const threadID in convertedThreadInfos) { const key = `threadInfo|${threadID}`; const hashResult = hashResults[key]; if (hashResult === null || hashResult === undefined) { hashResults[key] = false; } } } if (failUnmentioned && failUnmentioned.entryInfos) { for (const entryID in convertedEntryInfos) { const key = `entryInfo|${entryID}`; const hashResult = hashResults[key]; if (hashResult === null || hashResult === undefined) { hashResults[key] = false; } } } if (failUnmentioned && failUnmentioned.userInfos) { for (const userID in convertedUserInfos) { const key = `userInfo|${userID}`; const hashResult = hashResults[key]; if (hashResult === null || hashResult === undefined) { hashResults[key] = false; } } } clientResponses.push({ type: serverRequestTypes.CHECK_STATE, hashResults, }); } else if ( serverRequest.type === serverRequestTypes.MORE_ONE_TIME_KEYS && oneTimeKeyGenerator ) { const keys: string[] = []; for (let i = 0; i < minimumOneTimeKeysRequired; ++i) { keys.push(oneTimeKeyGenerator(i)); } clientResponses.push({ type: serverRequestTypes.MORE_ONE_TIME_KEYS, keys, }); } else if ( serverRequest.type === serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB && getSignedIdentityKeysBlob ) { const signedIdentityKeysBlob = await getSignedIdentityKeysBlob(); clientResponses.push({ type: serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB, signedIdentityKeysBlob, }); } else if ( serverRequest.type === serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE && getInitialNotificationsEncryptedMessage ) { const initialNotificationsEncryptedMessage = await getInitialNotificationsEncryptedMessage(); clientResponses.push({ type: serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE, initialNotificationsEncryptedMessage, }); } } return clientResponses; }; }, ); const sessionStateFuncSelector: ( state: AppState, ) => (calendarActive: boolean) => SessionState = createSelector( currentAsOfSelector, updatesCurrentAsOfSelector, currentCalendarQuery, ( messagesCurrentAsOf: number, updatesCurrentAsOf: number, calendarQuery: (calendarActive: boolean) => CalendarQuery, ) => (calendarActive: boolean): SessionState => ({ calendarQuery: calendarQuery(calendarActive), messagesCurrentAsOf, updatesCurrentAsOf, watchedIDs: threadWatcher.getWatchedIDs(), }), ); export { queuedReports, getClientResponsesSelector, sessionStateFuncSelector }; diff --git a/lib/utils/validation-utils.js b/lib/utils/conversion-utils.js similarity index 51% copy from lib/utils/validation-utils.js copy to lib/utils/conversion-utils.js index ac2d318cc..0349740d8 100644 --- a/lib/utils/validation-utils.js +++ b/lib/utils/conversion-utils.js @@ -1,251 +1,127 @@ // @flow -import invariant from 'invariant'; import _mapKeys from 'lodash/fp/mapKeys.js'; import _mapValues from 'lodash/fp/mapValues.js'; -import t from 'tcomb'; -import type { - TStructProps, - TIrreducible, - TRefinement, - TEnums, - TInterface, - TUnion, - TType, -} from 'tcomb'; +import type { TInterface, TType } from 'tcomb'; -import { - validEmailRegex, - oldValidUsernameRegex, - validHexColorRegex, -} from '../shared/account-utils.js'; -import type { PlatformDetails } from '../types/device-types'; -import type { - MediaMessageServerDBContent, - PhotoMessageServerDBContent, - VideoMessageServerDBContent, -} from '../types/messages/media'; - -function tBool(value: boolean): TIrreducible { - return t.irreducible(value.toString(), x => x === value); -} - -function tString(value: string): TIrreducible { - return t.irreducible(`'${value}'`, x => x === value); -} - -function tNumber(value: number): TIrreducible { - return t.irreducible(value.toString(), x => x === value); -} - -function tShape(spec: TStructProps): TInterface { - return t.interface(spec, { strict: true }); -} - -type TRegex = TRefinement; -function tRegex(regex: RegExp): TRegex { - return t.refinement(t.String, val => regex.test(val)); -} - -function tNumEnum(nums: $ReadOnlyArray): TRefinement { - return t.refinement(t.Number, (input: number) => { - for (const num of nums) { - if (input === num) { - return true; - } - } - return false; - }); -} -const tNull: TIrreducible = t.irreducible('null', x => x === null); -const tDate: TRegex = tRegex(/^[0-9]{4}-[0-1][0-9]-[0-3][0-9]$/); -const tColor: TRegex = tRegex(validHexColorRegex); // we don't include # char -const tPlatform: TEnums = t.enums.of([ - 'ios', - 'android', - 'web', - 'windows', - 'macos', -]); -const tDeviceType: TEnums = t.enums.of(['ios', 'android']); -const tPlatformDetails: TInterface = tShape({ - platform: tPlatform, - codeVersion: t.maybe(t.Number), - stateVersion: t.maybe(t.Number), -}); -const tPassword: TRefinement = t.refinement( - t.String, - (password: string) => !!password, -); -const tCookie: TRegex = tRegex(/^(user|anonymous)=[0-9]+:[0-9a-f]+$/); -const tEmail: TRegex = tRegex(validEmailRegex); -const tOldValidUsername: TRegex = tRegex(oldValidUsernameRegex); -const tID: TRefinement = t.refinement(t.String, (id: string) => !!id); - -const tMediaMessagePhoto: TInterface = tShape({ - type: tString('photo'), - uploadID: tID, -}); - -const tMediaMessageVideo: TInterface = tShape({ - type: tString('video'), - uploadID: tID, - thumbnailUploadID: tID, -}); - -const tMediaMessageMedia: TUnion = t.union([ - tMediaMessagePhoto, - tMediaMessageVideo, -]); - -function assertWithValidator(data: mixed, validator: TType): T { - invariant(validator.is(data), "data isn't of type T"); - return (data: any); -} - -const ashoatKeyserverID = '256'; +import { assertWithValidator, tID } from './validation-utils.js'; function convertServerIDsToClientIDs( serverPrefixID: string, outputValidator: TType, data: T, ): T { const conversionFunction = id => { if (id.indexOf('|') !== -1) { console.warn(`Server id '${id}' already has a prefix`); return id; } return `${serverPrefixID}|${id}`; }; return convertObject(outputValidator, data, [tID], conversionFunction); } function convertClientIDsToServerIDs( serverPrefixID: string, outputValidator: TType, data: T, ): T { const prefix = serverPrefixID + '|'; const conversionFunction = id => { if (id.startsWith(prefix)) { return id.substr(prefix.length); } throw new Error('invalid_client_id_prefix'); }; return convertObject(outputValidator, data, [tID], conversionFunction); } function convertObject( validator: TType, input: I, typesToConvert: $ReadOnlyArray>, conversionFunction: T => T, ): I { if (input === null || input === undefined) { return input; } // While they should be the same runtime object, // `TValidator` is `TType` and `validator` is `TType`. // Having them have different types allows us to use `assertWithValidator` // to change `input` flow type const TValidator = typesToConvert[typesToConvert.indexOf(validator)]; if (TValidator && TValidator.is(input)) { const TInput = assertWithValidator(input, TValidator); const converted = conversionFunction(TInput); return assertWithValidator(converted, validator); } if (validator.meta.kind === 'maybe' || validator.meta.kind === 'subtype') { return convertObject( validator.meta.type, input, typesToConvert, conversionFunction, ); } if (validator.meta.kind === 'interface' && typeof input === 'object') { const recastValidator: TInterface = (validator: any); const result = {}; for (const key in input) { const innerValidator = recastValidator.meta.props[key]; result[key] = convertObject( innerValidator, input[key], typesToConvert, conversionFunction, ); } return assertWithValidator(result, recastValidator); } if (validator.meta.kind === 'union') { for (const innerValidator of validator.meta.types) { if (innerValidator.is(input)) { return convertObject( innerValidator, input, typesToConvert, conversionFunction, ); } } return input; } if (validator.meta.kind === 'list' && Array.isArray(input)) { const innerValidator = validator.meta.type; return (input.map(value => convertObject(innerValidator, value, typesToConvert, conversionFunction), ): any); } if (validator.meta.kind === 'dict' && typeof input === 'object') { const domainValidator = validator.meta.domain; const codomainValidator = validator.meta.codomain; if (typesToConvert.includes(domainValidator)) { input = _mapKeys(key => conversionFunction(key))(input); } return _mapValues(value => convertObject( codomainValidator, value, typesToConvert, conversionFunction, ), )(input); } return input; } -const idSchemaRegex = '(?:[0-9]+\\|)?[0-9]+'; - export { - tBool, - tString, - tNumber, - tShape, - tRegex, - tNumEnum, - tNull, - tDate, - tColor, - tPlatform, - tDeviceType, - tPlatformDetails, - tPassword, - tCookie, - tEmail, - tOldValidUsername, - tID, - tMediaMessagePhoto, - tMediaMessageVideo, - tMediaMessageMedia, - assertWithValidator, - ashoatKeyserverID, convertClientIDsToServerIDs, convertServerIDsToClientIDs, convertObject, - idSchemaRegex, }; diff --git a/lib/utils/conversion-utils.test.js b/lib/utils/conversion-utils.test.js new file mode 100644 index 000000000..ac30d6449 --- /dev/null +++ b/lib/utils/conversion-utils.test.js @@ -0,0 +1,69 @@ +// @flow + +import invariant from 'invariant'; +import t from 'tcomb'; + +import { + convertServerIDsToClientIDs, + convertClientIDsToServerIDs, +} from './conversion-utils.js'; +import { tShape, tID, idSchemaRegex } from './validation-utils.js'; + +describe('id conversion', () => { + it('should convert string id', () => { + const validator = tShape({ id: tID }); + const serverData = { id: '1' }; + const clientData = { id: '0|1' }; + + expect( + convertServerIDsToClientIDs('0', validator, serverData), + ).toStrictEqual(clientData); + expect( + convertClientIDsToServerIDs('0', validator, clientData), + ).toStrictEqual(serverData); + }); + + it('should convert a complex type', () => { + const validator = tShape({ ids: t.dict(tID, t.list(tID)) }); + const serverData = { ids: { '1': ['11', '12'], '2': [], '3': ['13'] } }; + const clientData = { + ids: { '0|1': ['0|11', '0|12'], '0|2': [], '0|3': ['0|13'] }, + }; + + expect( + convertServerIDsToClientIDs('0', validator, serverData), + ).toStrictEqual(clientData); + expect( + convertClientIDsToServerIDs('0', validator, clientData), + ).toStrictEqual(serverData); + }); + + it('should convert a refinement', () => { + const validator = t.refinement(tID, () => true); + const serverData = '1'; + const clientData = '0|1'; + + expect( + convertServerIDsToClientIDs('0', validator, serverData), + ).toStrictEqual(clientData); + expect( + convertClientIDsToServerIDs('0', validator, clientData), + ).toStrictEqual(serverData); + }); +}); + +describe('idSchemaRegex tests', () => { + it('should capture ids', () => { + const regex = new RegExp(`^(${idSchemaRegex})$`); + const ids = ['123|123', '0|0', '123', '0']; + + for (const id of ids) { + const result = regex.exec(id); + expect(result).not.toBeNull(); + invariant(result, 'result is not null'); + const matches = [...result]; + expect(matches).toHaveLength(2); + expect(matches[1]).toBe(id); + } + }); +}); diff --git a/lib/utils/validation-utils.js b/lib/utils/validation-utils.js index ac2d318cc..08f3db2c9 100644 --- a/lib/utils/validation-utils.js +++ b/lib/utils/validation-utils.js @@ -1,251 +1,132 @@ // @flow import invariant from 'invariant'; -import _mapKeys from 'lodash/fp/mapKeys.js'; -import _mapValues from 'lodash/fp/mapValues.js'; import t from 'tcomb'; import type { TStructProps, TIrreducible, TRefinement, TEnums, TInterface, TUnion, TType, } from 'tcomb'; import { validEmailRegex, oldValidUsernameRegex, validHexColorRegex, } from '../shared/account-utils.js'; import type { PlatformDetails } from '../types/device-types'; import type { MediaMessageServerDBContent, PhotoMessageServerDBContent, VideoMessageServerDBContent, } from '../types/messages/media'; function tBool(value: boolean): TIrreducible { return t.irreducible(value.toString(), x => x === value); } function tString(value: string): TIrreducible { return t.irreducible(`'${value}'`, x => x === value); } function tNumber(value: number): TIrreducible { return t.irreducible(value.toString(), x => x === value); } function tShape(spec: TStructProps): TInterface { return t.interface(spec, { strict: true }); } type TRegex = TRefinement; function tRegex(regex: RegExp): TRegex { return t.refinement(t.String, val => regex.test(val)); } function tNumEnum(nums: $ReadOnlyArray): TRefinement { return t.refinement(t.Number, (input: number) => { for (const num of nums) { if (input === num) { return true; } } return false; }); } const tNull: TIrreducible = t.irreducible('null', x => x === null); const tDate: TRegex = tRegex(/^[0-9]{4}-[0-1][0-9]-[0-3][0-9]$/); const tColor: TRegex = tRegex(validHexColorRegex); // we don't include # char const tPlatform: TEnums = t.enums.of([ 'ios', 'android', 'web', 'windows', 'macos', ]); const tDeviceType: TEnums = t.enums.of(['ios', 'android']); const tPlatformDetails: TInterface = tShape({ platform: tPlatform, codeVersion: t.maybe(t.Number), stateVersion: t.maybe(t.Number), }); const tPassword: TRefinement = t.refinement( t.String, (password: string) => !!password, ); const tCookie: TRegex = tRegex(/^(user|anonymous)=[0-9]+:[0-9a-f]+$/); const tEmail: TRegex = tRegex(validEmailRegex); const tOldValidUsername: TRegex = tRegex(oldValidUsernameRegex); const tID: TRefinement = t.refinement(t.String, (id: string) => !!id); const tMediaMessagePhoto: TInterface = tShape({ type: tString('photo'), uploadID: tID, }); const tMediaMessageVideo: TInterface = tShape({ type: tString('video'), uploadID: tID, thumbnailUploadID: tID, }); const tMediaMessageMedia: TUnion = t.union([ tMediaMessagePhoto, tMediaMessageVideo, ]); function assertWithValidator(data: mixed, validator: TType): T { invariant(validator.is(data), "data isn't of type T"); return (data: any); } const ashoatKeyserverID = '256'; -function convertServerIDsToClientIDs( - serverPrefixID: string, - outputValidator: TType, - data: T, -): T { - const conversionFunction = id => { - if (id.indexOf('|') !== -1) { - console.warn(`Server id '${id}' already has a prefix`); - return id; - } - return `${serverPrefixID}|${id}`; - }; - - return convertObject(outputValidator, data, [tID], conversionFunction); -} - -function convertClientIDsToServerIDs( - serverPrefixID: string, - outputValidator: TType, - data: T, -): T { - const prefix = serverPrefixID + '|'; - const conversionFunction = id => { - if (id.startsWith(prefix)) { - return id.substr(prefix.length); - } - - throw new Error('invalid_client_id_prefix'); - }; - - return convertObject(outputValidator, data, [tID], conversionFunction); -} - -function convertObject( - validator: TType, - input: I, - typesToConvert: $ReadOnlyArray>, - conversionFunction: T => T, -): I { - if (input === null || input === undefined) { - return input; - } - - // While they should be the same runtime object, - // `TValidator` is `TType` and `validator` is `TType`. - // Having them have different types allows us to use `assertWithValidator` - // to change `input` flow type - const TValidator = typesToConvert[typesToConvert.indexOf(validator)]; - if (TValidator && TValidator.is(input)) { - const TInput = assertWithValidator(input, TValidator); - const converted = conversionFunction(TInput); - return assertWithValidator(converted, validator); - } - - if (validator.meta.kind === 'maybe' || validator.meta.kind === 'subtype') { - return convertObject( - validator.meta.type, - input, - typesToConvert, - conversionFunction, - ); - } - if (validator.meta.kind === 'interface' && typeof input === 'object') { - const recastValidator: TInterface = (validator: any); - const result = {}; - for (const key in input) { - const innerValidator = recastValidator.meta.props[key]; - result[key] = convertObject( - innerValidator, - input[key], - typesToConvert, - conversionFunction, - ); - } - return assertWithValidator(result, recastValidator); - } - if (validator.meta.kind === 'union') { - for (const innerValidator of validator.meta.types) { - if (innerValidator.is(input)) { - return convertObject( - innerValidator, - input, - typesToConvert, - conversionFunction, - ); - } - } - return input; - } - if (validator.meta.kind === 'list' && Array.isArray(input)) { - const innerValidator = validator.meta.type; - return (input.map(value => - convertObject(innerValidator, value, typesToConvert, conversionFunction), - ): any); - } - if (validator.meta.kind === 'dict' && typeof input === 'object') { - const domainValidator = validator.meta.domain; - const codomainValidator = validator.meta.codomain; - if (typesToConvert.includes(domainValidator)) { - input = _mapKeys(key => conversionFunction(key))(input); - } - return _mapValues(value => - convertObject( - codomainValidator, - value, - typesToConvert, - conversionFunction, - ), - )(input); - } - - return input; -} - const idSchemaRegex = '(?:[0-9]+\\|)?[0-9]+'; export { tBool, tString, tNumber, tShape, tRegex, tNumEnum, tNull, tDate, tColor, tPlatform, tDeviceType, tPlatformDetails, tPassword, tCookie, tEmail, tOldValidUsername, tID, tMediaMessagePhoto, tMediaMessageVideo, tMediaMessageMedia, assertWithValidator, ashoatKeyserverID, - convertClientIDsToServerIDs, - convertServerIDsToClientIDs, - convertObject, idSchemaRegex, }; diff --git a/lib/utils/validation-utils.test.js b/lib/utils/validation-utils.test.js index 8e28eb7c6..963c9b133 100644 --- a/lib/utils/validation-utils.test.js +++ b/lib/utils/validation-utils.test.js @@ -1,209 +1,142 @@ // @flow -import invariant from 'invariant'; -import t from 'tcomb'; - import { tMediaMessagePhoto, tMediaMessageVideo, tNumEnum, - tShape, - tID, - convertServerIDsToClientIDs, - convertClientIDsToServerIDs, - idSchemaRegex, } from './validation-utils.js'; import { threadTypes } from '../types/thread-types-enum.js'; import { values } from '../utils/objects.js'; describe('Validation utils', () => { describe('tNumEnum validator', () => { it('Should discard when accepted set is empty', () => { expect(tNumEnum([]).is(1)).toBe(false); }); it('Should accept when array contains number', () => { expect(tNumEnum([1, 2, 3]).is(2)).toBe(true); }); it('Should discard when array does not contain number', () => { expect(tNumEnum([1, 2, 3]).is(4)).toBe(false); }); it('Should accept when value is a part of enum', () => { expect(tNumEnum(values(threadTypes)).is(threadTypes.SIDEBAR)).toBe(true); }); it('Should discard when value is not a part of enum', () => { expect(tNumEnum(values(threadTypes)).is(123)).toBe(false); }); }); describe('tMediaMessagePhoto validator', () => { it('Should succeed when valid MediaMessagePhoto', () => { expect(tMediaMessagePhoto.is({ type: 'photo', uploadID: '12345' })).toBe( true, ); }); it('Should fail when missing uploadID', () => { expect(tMediaMessagePhoto.is({ type: 'photo' })).toBe(false); }); it('Should fail when uploadID is not a string', () => { expect(tMediaMessagePhoto.is({ type: 'photo', uploadID: 12345 })).toBe( false, ); }); it('Should fail when type is not photo', () => { expect(tMediaMessagePhoto.is({ type: 'blah', uploadID: '12345' })).toBe( false, ); }); it('Should fail when type is video', () => { expect(tMediaMessagePhoto.is({ type: 'video', uploadID: '12345' })).toBe( false, ); }); }); describe('tMediaMessageVideo validator', () => { it('Should succeed when valid tMediaMessageVideo', () => { expect( tMediaMessageVideo.is({ type: 'video', uploadID: '12345', thumbnailUploadID: '7890', }), ).toBe(true); }); it('Should fail when missing thumbnailUploadID', () => { expect( tMediaMessageVideo.is({ type: 'video', uploadID: '12345', }), ).toBe(false); }); it('Should fail when uploadID is not a string', () => { expect( tMediaMessageVideo.is({ type: 'video', uploadID: 12345, thumbnailUploadID: '7890', }), ).toBe(false); }); it('Should fail when type is not video', () => { expect( tMediaMessageVideo.is({ type: 'blah', uploadID: '12345', thumbnailUploadID: '7890', }), ).toBe(false); }); it('Should fail when type is photo', () => { expect( tMediaMessageVideo.is({ type: 'photo', uploadID: '12345', thumbnailUploadID: '7890', }), ).toBe(false); }); }); describe('tMediaMessageMedia validator', () => { it('Should succeed when valid MediaMessagePhoto', () => { expect(tMediaMessagePhoto.is({ type: 'photo', uploadID: '12345' })).toBe( true, ); }); it('Should succeed when valid tMediaMessageVideo', () => { expect( tMediaMessageVideo.is({ type: 'video', uploadID: '12345', thumbnailUploadID: '7890', }), ).toBe(true); }); it('Should fail when not valid MediaMessagePhoto or tMediaMessageVideo', () => { expect( tMediaMessageVideo.is({ type: 'audio', uploadID: '1000', thumbnailUploadID: '1000', }), ).toBe(false); }); }); }); - -describe('id conversion', () => { - it('should convert string id', () => { - const validator = tShape({ id: tID }); - const serverData = { id: '1' }; - const clientData = { id: '0|1' }; - - expect( - convertServerIDsToClientIDs('0', validator, serverData), - ).toStrictEqual(clientData); - expect( - convertClientIDsToServerIDs('0', validator, clientData), - ).toStrictEqual(serverData); - }); - - it('should convert a complex type', () => { - const validator = tShape({ ids: t.dict(tID, t.list(tID)) }); - const serverData = { ids: { '1': ['11', '12'], '2': [], '3': ['13'] } }; - const clientData = { - ids: { '0|1': ['0|11', '0|12'], '0|2': [], '0|3': ['0|13'] }, - }; - - expect( - convertServerIDsToClientIDs('0', validator, serverData), - ).toStrictEqual(clientData); - expect( - convertClientIDsToServerIDs('0', validator, clientData), - ).toStrictEqual(serverData); - }); - - it('should convert a refinement', () => { - const validator = t.refinement(tID, () => true); - const serverData = '1'; - const clientData = '0|1'; - - expect( - convertServerIDsToClientIDs('0', validator, serverData), - ).toStrictEqual(clientData); - expect( - convertClientIDsToServerIDs('0', validator, clientData), - ).toStrictEqual(serverData); - }); -}); - -describe('idSchemaRegex tests', () => { - it('should capture ids', () => { - const regex = new RegExp(`^(${idSchemaRegex})$`); - const ids = ['123|123', '0|0', '123', '0']; - - for (const id of ids) { - const result = regex.exec(id); - expect(result).not.toBeNull(); - invariant(result, 'result is not null'); - const matches = [...result]; - expect(matches).toHaveLength(2); - expect(matches[1]).toBe(id); - } - }); -});