diff --git a/keyserver/src/responders/entry-responders.js b/keyserver/src/responders/entry-responders.js --- a/keyserver/src/responders/entry-responders.js +++ b/keyserver/src/responders/entry-responders.js @@ -4,28 +4,33 @@ import type { TInterface } from 'tcomb'; import { filteredThreadIDs } from 'lib/selectors/calendar-filter-selectors.js'; -import type { - CalendarQuery, - SaveEntryRequest, - CreateEntryRequest, - DeleteEntryRequest, - DeleteEntryResponse, - RestoreEntryRequest, - RestoreEntryResponse, - FetchEntryInfosResponse, - DeltaEntryInfosResult, - SaveEntryResponse, +import { + type CalendarQuery, + type SaveEntryRequest, + type CreateEntryRequest, + type DeleteEntryRequest, + type DeleteEntryResponse, + type RestoreEntryRequest, + type RestoreEntryResponse, + type FetchEntryInfosResponse, + type DeltaEntryInfosResult, + type SaveEntryResponse, + rawEntryInfoValidator, } from 'lib/types/entry-types.js'; import { type CalendarFilter, calendarThreadFilterTypes, } from 'lib/types/filter-types.js'; -import type { - FetchEntryRevisionInfosResult, - FetchEntryRevisionInfosRequest, +import { + type FetchEntryRevisionInfosResult, + type FetchEntryRevisionInfosRequest, + historyRevisionInfoValidator, } from 'lib/types/history-types.js'; +import { rawMessageInfoValidator } from 'lib/types/message-types.js'; +import { serverCreateUpdatesResponseValidator } from 'lib/types/update-types.js'; +import { accountUserInfoValidator } from 'lib/types/user-types.js'; import { ServerError } from 'lib/utils/errors.js'; -import { tString, tShape, tDate } from 'lib/utils/validation-utils.js'; +import { tString, tShape, tDate, tID } from 'lib/utils/validation-utils.js'; import createEntry from '../creators/entry-creator.js'; import { deleteEntry, restoreEntry } from '../deleters/entry-deleters.js'; @@ -123,6 +128,12 @@ } } +export const fetchEntryInfosResponseValidator: TInterface = + tShape({ + rawEntryInfos: t.list(rawEntryInfoValidator), + userInfos: t.dict(t.String, accountUserInfoValidator), + }); + async function entryFetchResponder( viewer: Viewer, input: any, @@ -140,6 +151,11 @@ id: t.String, }); +export const fetchEntryRevisionInfosResultValidator: TInterface = + tShape({ + result: t.list(historyRevisionInfoValidator), + }); + async function entryRevisionFetchResponder( viewer: Viewer, input: any, @@ -160,6 +176,13 @@ calendarQuery: t.maybe(newEntryQueryInputValidator), }); +export const saveEntryResponseValidator: TInterface = + tShape({ + entryID: tID, + newMessageInfos: t.list(rawMessageInfoValidator), + updatesResult: serverCreateUpdatesResponseValidator, + }); + async function entryCreationResponder( viewer: Viewer, input: any, @@ -195,6 +218,13 @@ calendarQuery: t.maybe(newEntryQueryInputValidator), }); +export const deleteEntryResponseValidator: TInterface = + tShape({ + newMessageInfos: t.list(rawMessageInfoValidator), + threadID: tID, + updatesResult: serverCreateUpdatesResponseValidator, + }); + async function entryDeletionResponder( viewer: Viewer, input: any, @@ -211,6 +241,12 @@ calendarQuery: t.maybe(newEntryQueryInputValidator), }); +export const restoreEntryResponseValidator: TInterface = + tShape({ + newMessageInfos: t.list(rawMessageInfoValidator), + updatesResult: serverCreateUpdatesResponseValidator, + }); + async function entryRestorationResponder( viewer: Viewer, input: any, @@ -220,6 +256,13 @@ return await restoreEntry(viewer, request); } +export const deltaEntryInfosResultValidator: TInterface = + tShape({ + rawEntryInfos: t.list(rawEntryInfoValidator), + deletedEntryIDs: t.list(tID), + userInfos: t.list(accountUserInfoValidator), + }); + async function calendarQueryUpdateResponder( viewer: Viewer, input: any, diff --git a/keyserver/src/responders/keys-responders.js b/keyserver/src/responders/keys-responders.js --- a/keyserver/src/responders/keys-responders.js +++ b/keyserver/src/responders/keys-responders.js @@ -1,10 +1,13 @@ // @flow -import t from 'tcomb'; +import t, { type TUnion } from 'tcomb'; import type { GetSessionPublicKeysArgs } from 'lib/types/request-types.js'; -import type { SessionPublicKeys } from 'lib/types/session-types.js'; -import { tShape } from 'lib/utils/validation-utils.js'; +import { + type SessionPublicKeys, + sessionPublicKeysValidator, +} from 'lib/types/session-types.js'; +import { tShape, tNull } from 'lib/utils/validation-utils.js'; import { fetchSessionPublicKeys } from '../fetchers/key-fetchers.js'; import type { Viewer } from '../session/viewer.js'; @@ -14,10 +17,14 @@ session: t.String, }); +type GetSessionPublicKeysResponse = SessionPublicKeys | null; +export const getSessionPublicKeysResponseValidator: TUnion = + t.union([sessionPublicKeysValidator, tNull]); + async function getSessionPublicKeysResponder( viewer: Viewer, input: any, -): Promise { +): Promise { if (!viewer.loggedIn) { return null; } diff --git a/keyserver/src/responders/message-report-responder.js b/keyserver/src/responders/message-report-responder.js --- a/keyserver/src/responders/message-report-responder.js +++ b/keyserver/src/responders/message-report-responder.js @@ -1,11 +1,12 @@ // @flow -import t from 'tcomb'; +import t, { type TInterface } from 'tcomb'; import { type MessageReportCreationRequest, type MessageReportCreationResult, } from 'lib/types/message-report-types.js'; +import { rawMessageInfoValidator } from 'lib/types/message-types.js'; import { tShape } from 'lib/utils/validation-utils.js'; import createMessageReport from '../creators/message-report-creator.js'; @@ -16,6 +17,9 @@ messageID: t.String, }); +export const messageReportCreationResultValidator: TInterface = + tShape({ messageInfo: rawMessageInfoValidator }); + async function messageReportCreationResponder( viewer: Viewer, input: any, diff --git a/keyserver/src/responders/relationship-responders.js b/keyserver/src/responders/relationship-responders.js --- a/keyserver/src/responders/relationship-responders.js +++ b/keyserver/src/responders/relationship-responders.js @@ -1,6 +1,6 @@ // @flow -import t from 'tcomb'; +import t, { type TInterface } from 'tcomb'; import { type RelationshipRequest, @@ -18,6 +18,13 @@ userIDs: t.list(t.String), }); +export const relationshipErrorsValidator: TInterface = + tShape({ + invalid_user: t.maybe(t.list(t.String)), + already_friends: t.maybe(t.list(t.String)), + user_blocked: t.maybe(t.list(t.String)), + }); + async function updateRelationshipsResponder( viewer: Viewer, input: any, diff --git a/keyserver/src/responders/responder-validators.test.js b/keyserver/src/responders/responder-validators.test.js --- a/keyserver/src/responders/responder-validators.test.js +++ b/keyserver/src/responders/responder-validators.test.js @@ -1,5 +1,23 @@ // @flow +import { + setThreadUnreadStatusResult, + 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 { messageReportCreationResultValidator } from './message-report-responder.js'; +import { relationshipErrorsValidator } from './relationship-responders.js'; +import { userSearchResultValidator } from './search-responders.js'; +import { siweNonceResponseValidator } from './siwe-nonce-responders.js'; import { logInResponseValidator, registerResponseValidator, @@ -331,3 +349,271 @@ ).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(setThreadUnreadStatusResult.is(response)).toBe(true); + expect(setThreadUnreadStatusResult.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); + }); +}); diff --git a/keyserver/src/responders/search-responders.js b/keyserver/src/responders/search-responders.js --- a/keyserver/src/responders/search-responders.js +++ b/keyserver/src/responders/search-responders.js @@ -1,11 +1,12 @@ // @flow -import t from 'tcomb'; +import t, { type TInterface } from 'tcomb'; import type { UserSearchRequest, UserSearchResult, } from 'lib/types/search-types.js'; +import { globalAccountUserInfoValidator } from 'lib/types/user-types.js'; import { tShape } from 'lib/utils/validation-utils.js'; import { searchForUsers } from '../search/users.js'; @@ -16,6 +17,11 @@ prefix: t.maybe(t.String), }); +export const userSearchResultValidator: TInterface = + tShape({ + userInfos: t.list(globalAccountUserInfoValidator), + }); + async function userSearchResponder( viewer: Viewer, input: any, diff --git a/keyserver/src/responders/siwe-nonce-responders.js b/keyserver/src/responders/siwe-nonce-responders.js --- a/keyserver/src/responders/siwe-nonce-responders.js +++ b/keyserver/src/responders/siwe-nonce-responders.js @@ -1,11 +1,16 @@ // @flow import { generateNonce } from 'siwe'; +import t, { type TInterface } from 'tcomb'; import type { SIWENonceResponse } from 'lib/types/siwe-types.js'; +import { tShape } from 'lib/utils/validation-utils.js'; import { createSIWENonceEntry } from '../creators/siwe-nonce-creator.js'; +export const siweNonceResponseValidator: TInterface = + tShape({ nonce: t.String }); + async function siweNonceResponder(): Promise { const generatedNonce = generateNonce(); await createSIWENonceEntry(generatedNonce); diff --git a/lib/types/activity-types.js b/lib/types/activity-types.js --- a/lib/types/activity-types.js +++ b/lib/types/activity-types.js @@ -1,5 +1,9 @@ // @flow +import t, { type TInterface } from 'tcomb'; + +import { tID, tShape } from '../utils/validation-utils.js'; + export type ActivityUpdate = { +focus: boolean, +threadID: string, @@ -13,6 +17,10 @@ export type UpdateActivityResult = { +unfocusedToUnread: $ReadOnlyArray, }; +export const updateActivityResultValidator: TInterface = + tShape({ + unfocusedToUnread: t.list(tID), + }); export type ActivityUpdateSuccessPayload = { +activityUpdates: $ReadOnlyArray, @@ -32,6 +40,9 @@ export type SetThreadUnreadStatusResult = { +resetToUnread: boolean, }; +export const setThreadUnreadStatusResult: TInterface = + tShape({ resetToUnread: t.Boolean }); + export type SetThreadUnreadStatusPayload = { ...SetThreadUnreadStatusResult, +threadID: string, diff --git a/lib/types/history-types.js b/lib/types/history-types.js --- a/lib/types/history-types.js +++ b/lib/types/history-types.js @@ -1,5 +1,9 @@ // @flow +import t, { type TInterface } from 'tcomb'; + +import { tID, tShape } from '../utils/validation-utils.js'; + export type HistoryMode = 'day' | 'entry'; export type HistoryRevisionInfo = { @@ -11,6 +15,16 @@ +deleted: boolean, +threadID: string, }; +export const historyRevisionInfoValidator: TInterface = + tShape({ + id: tID, + entryID: tID, + authorID: t.String, + text: t.String, + lastUpdate: t.Number, + deleted: t.Boolean, + threadID: tID, + }); export type FetchEntryRevisionInfosRequest = { +id: string, diff --git a/lib/types/session-types.js b/lib/types/session-types.js --- a/lib/types/session-types.js +++ b/lib/types/session-types.js @@ -1,5 +1,7 @@ // @flow +import t, { type TInterface } from 'tcomb'; + import type { LogInActionSource } from './account-types.js'; import type { Shape } from './core.js'; import type { CalendarQuery } from './entry-types.js'; @@ -9,6 +11,7 @@ type CurrentUserInfo, type LoggedOutUserInfo, } from './user-types.js'; +import { tShape } from '../utils/validation-utils.js'; export const cookieLifetime = 30 * 24 * 60 * 60 * 1000; // in milliseconds // Interval the server waits after a state check before starting a new one @@ -107,3 +110,9 @@ +identityKey: string, +oneTimeKey?: string, }; + +export const sessionPublicKeysValidator: TInterface = + tShape({ + identityKey: t.String, + oneTimeKey: t.maybe(t.String), + }); diff --git a/lib/types/update-types.js b/lib/types/update-types.js --- a/lib/types/update-types.js +++ b/lib/types/update-types.js @@ -13,6 +13,7 @@ import { type RawThreadInfo, rawThreadInfoValidator } from './thread-types.js'; import { type UserInfo, + userInfoValidator, type UserInfos, type LoggedInUserInfo, loggedInUserInfoValidator, @@ -400,6 +401,12 @@ +userInfos: $ReadOnlyArray, }; +export const serverCreateUpdatesResponseValidator: TInterface = + tShape({ + viewerUpdates: t.list(serverUpdateInfoValidator), + userInfos: t.list(userInfoValidator), + }); + export type ClientCreateUpdatesResponse = { +viewerUpdates: $ReadOnlyArray, +userInfos: $ReadOnlyArray, diff --git a/lib/types/user-types.js b/lib/types/user-types.js --- a/lib/types/user-types.js +++ b/lib/types/user-types.js @@ -25,6 +25,12 @@ +username: string, +avatar?: ?ClientAvatar, }; +export const globalAccountUserInfoValidator: TInterface = + tShape({ + id: t.String, + username: t.String, + avatar: t.maybe(clientAvatarValidator), + }); export type UserInfo = { +id: string, @@ -46,6 +52,13 @@ +relationshipStatus?: UserRelationshipStatus, +avatar?: ?ClientAvatar, }; +export const accountUserInfoValidator: TInterface = + tShape({ + id: t.String, + username: t.String, + relationshipStatus: t.maybe(userRelationshipStatusValidator), + avatar: t.maybe(clientAvatarValidator), + }); export type UserStore = { +userInfos: UserInfos, diff --git a/lib/utils/validation-utils.js b/lib/utils/validation-utils.js --- a/lib/utils/validation-utils.js +++ b/lib/utils/validation-utils.js @@ -55,7 +55,7 @@ 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([ @@ -108,6 +108,7 @@ tShape, tRegex, tNumEnum, + tNull, tDate, tColor, tPlatform,