From b65b5493b66b97d49d39f6f018238cece7008cfc Mon Sep 17 00:00:00 2001 From: aksonov Date: Wed, 18 Mar 2020 18:12:47 +0300 Subject: [PATCH] WIP: base for #4883 --- __tests__/formValidation.test.ts | 120 ------------------------- __tests__/wocky/formValidation.test.ts | 107 ++++++++++++++++++++++ __tests__/wocky/support/setup.js | 9 ++ __tests__/wocky/support/testuser.ts | 9 -- src/components/FormTextInput.tsx | 18 +++- src/components/MyAccount.tsx | 26 +----- src/model/Profile.ts | 63 +++++++++++++ src/store/ProfileValidationStore.ts | 23 ----- src/store/store.ts | 3 - src/utils/ValidateItem.ts | 26 ------ src/utils/formValidation.ts | 95 -------------------- 11 files changed, 197 insertions(+), 302 deletions(-) delete mode 100644 __tests__/formValidation.test.ts create mode 100644 __tests__/wocky/formValidation.test.ts delete mode 100644 src/store/ProfileValidationStore.ts delete mode 100644 src/utils/ValidateItem.ts delete mode 100644 src/utils/formValidation.ts diff --git a/__tests__/formValidation.test.ts b/__tests__/formValidation.test.ts deleted file mode 100644 index b8b3a0b1e..000000000 --- a/__tests__/formValidation.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import validate from 'validate.js' -import {validateProfile} from '../src/utils/formValidation' - -declare module 'validate.js' { - // tslint:disable-next-line - interface ValidateJS { - Promise: any - } -} - -validate.validators.usernameUniqueValidator = value => { - // if (!value) return new validate.Promise(res => res()); - return new validate.Promise(resolve => { - resolve() - }) -} - -describe('form validation', () => { - it('happy path - English', async () => { - const obj = { - firstName: 'Jenny', - lastName: 'Ong', - handle: 'jeenee', - email: 'jenny@jenny.com', - } - const result = await validateProfile(obj) - expect(result).toStrictEqual(obj) - }) - - it('handle with underscore', async () => { - const obj = {handle: 'jee_nee'} - const result = await validateProfile(obj) - expect(result).toStrictEqual(obj) - }) - - it('short handle', async () => { - try { - await validateProfile({handle: 'tw'}) - } catch (err) { - expect(err).toStrictEqual({ - handle: ['Handle must be 3 - 16 characters'], - }) - } - }) - - it('hyphenated handle', async () => { - try { - await validateProfile({handle: 'hyphen-boy'}) - } catch (err) { - expect(err).toStrictEqual({ - handle: ['Handle can only contain alphanumeric characters and _'], - }) - } - }) - - it('handle with non Rmonan characters', async () => { - try { - await validateProfile({handle: '😀 -boy'}) - } catch (err) { - expect(err).toStrictEqual({ - handle: ['Handle can only contain alphanumeric characters and _'], - }) - } - }) - - it('first name starts with space', async () => { - try { - await validateProfile({firstName: ' Eric'}) - } catch (err) { - expect(err).toStrictEqual({ - firstName: ['First name is invalid'], - }) - } - }) - - it('first name ends with space', async () => { - try { - await validateProfile({firstName: 'Eric '}) - } catch (err) { - expect(err).toStrictEqual({ - firstName: ['First name can only contain alphabet characters'], - }) - } - }) - - it('first name with apostrophe', async () => { - const obj = {firstName: "Eric'apostrophe"} - const result = await validateProfile(obj) - expect(result).toStrictEqual(obj) - }) - - it('first name with numbers', async () => { - const obj = {firstName: 'Eric1234Kirkham'} - const result = await validateProfile(obj) - expect(result).toStrictEqual(obj) - }) - - it('bad email', async () => { - try { - await validateProfile({email: 'eric@e'}) - } catch (err) { - expect(err).toStrictEqual({ - email: ['Email is not a valid email'], - }) - } - }) - - it('first name with non-English characters', async () => { - const obj = {firstName: 'ÚБ見'} - const result = await validateProfile(obj) - expect(result).toStrictEqual(obj) - }) - - // #1503 - it('accepts short first name', async () => { - const obj = {firstName: 'T'} - const result = await validateProfile(obj) - expect(result).toStrictEqual(obj) - }) -}) diff --git a/__tests__/wocky/formValidation.test.ts b/__tests__/wocky/formValidation.test.ts new file mode 100644 index 000000000..dd955a179 --- /dev/null +++ b/__tests__/wocky/formValidation.test.ts @@ -0,0 +1,107 @@ +import validate from 'validate.js' +import {Profile} from '../../src/wocky' + +declare module 'validate.js' { + // tslint:disable-next-line + interface ValidateJS { + Promise: any + } +} + +const obj = { + id: '1', + firstName: 'Jenny', + lastName: 'Ong', + handle: 'jeenee', + email: 'jenny@jenny.com', +} + +validate.validators.usernameUniqueValidator = value => { + // if (!value) return new validate.Promise(res => res()); + return new validate.Promise(resolve => { + resolve() + }) +} + +describe('form validation', () => { + // it('happy path - English', async () => { + // const profile = Profile.create(obj) + // await profile.validate() + // expect(profile.isValid).toBeTruthy() + // }) + + // it('handle with underscore', async () => { + // const profile = Profile.create({...obj, handle: 'jee_nee'}) + // await profile.validate() + // expect(profile.isValid).toBeTruthy() + // }) + + it('short handle', async () => { + const profile = Profile.create({...obj, handle: 'je'}) + await profile.validate() + expect(profile.isValid).toBeFalsy() + expect(profile.errors).toStrictEqual({ + handle: ['Handle must be 3 - 16 characters'], + }) + }) + + it('hyphenated handle', async () => { + const profile = Profile.create({...obj, handle: 'hyphen-boy'}) + await profile.validate() + expect(profile.isValid).toBeFalsy() + expect(profile.errors).toStrictEqual({ + handle: ['Handle can only contain alphanumeric characters and _'], + }) + }) + + it('handle with non Rmonan characters', async () => { + const profile = Profile.create({...obj, handle: '😀 -boy'}) + await profile.validate() + expect(profile.isValid).toBeFalsy() + expect(profile.errors).toStrictEqual({ + handle: ['Handle can only contain alphanumeric characters and _'], + }) + }) + + it('first name starts with space', async () => { + const profile = Profile.create({...obj, firstName: ' Eric'}) + await profile.validate() + expect(profile.isValid).toBeFalsy() + expect(profile.errors).toStrictEqual({ + firstName: ['First name is invalid'], + }) + }) + + it('first name ends with space', async () => { + const profile = Profile.create({...obj, firstName: 'Eric '}) + await profile.validate() + expect(profile.isValid).toBeFalsy() + expect(profile.errors).toStrictEqual({ + firstName: ['First name can only contain alphabet characters'], + }) + }) + + // it('first name with apostrophe', async () => { + // const obj = {firstName: "Eric'apostrophe"} + // const result = await validateProfile(obj) + // expect(result).toStrictEqual(obj) + // }) + + // it('first name with numbers', async () => { + // const obj = {firstName: 'Eric1234Kirkham'} + // const result = await validateProfile(obj) + // expect(result).toStrictEqual(obj) + // }) + + // it('first name with non-English characters', async () => { + // const obj = {firstName: 'ÚБ見'} + // const result = await validateProfile(obj) + // expect(result).toStrictEqual(obj) + // }) + + // it('accepts short first name', async () => { + // const obj = {firstName: 'T'} + // const result = await validateProfile(obj) + // expect(result).toStrictEqual(obj) + // }) +}) diff --git a/__tests__/wocky/support/setup.js b/__tests__/wocky/support/setup.js index 0d9ad7ac4..1f1d345b8 100644 --- a/__tests__/wocky/support/setup.js +++ b/__tests__/wocky/support/setup.js @@ -1,3 +1,12 @@ process.env.NODE_ENV = 'test' global.__DEV__ = true global.WebSocket = require('websocket').w3cwebsocket + +jest.mock('../../../src/utils/logger', () => ({ + log: console.log, + warn: console.warn, + error: console.error, + assert: console.assert, + persistLog: () => null, + notifyBugsnag: () => null, +})) diff --git a/__tests__/wocky/support/testuser.ts b/__tests__/wocky/support/testuser.ts index 8481564c9..ac2813bf6 100644 --- a/__tests__/wocky/support/testuser.ts +++ b/__tests__/wocky/support/testuser.ts @@ -14,15 +14,6 @@ const SERVER_NAME = 'testing' // tslint:disable:no-console const fs = require('fs') -jest.mock('../../../src/utils/logger', () => ({ - log: console.log, - warn: console.warn, - error: console.error, - assert: console.assert, - persistLog: () => null, - notifyBugsnag: () => null, -})) - function token(credentials: any) { const payload = { aud: 'Wocky', diff --git a/src/components/FormTextInput.tsx b/src/components/FormTextInput.tsx index ab6519dc6..721360895 100644 --- a/src/components/FormTextInput.tsx +++ b/src/components/FormTextInput.tsx @@ -3,19 +3,29 @@ import {Image, View, TextInputProperties, TouchableOpacity, Platform} from 'reac import {k} from './Global' import {colors} from '../constants' import {observer} from 'mobx-react' -import {ValidateItem} from '../utils/formValidation' import {RText, RTextInput, Separator} from './common' import Cell from './Cell' +import {IWocky} from 'src/wocky' +import {inject} from 'mobx-react' interface IProps extends TextInputProperties { icon?: any label: string - store?: ValidateItem + name: string + wocky?: IWocky imageStyle?: any } +@observer +@inject('wocky') export class FormTextInput extends React.Component { input: any + errorMessage: string = '' + + constructor(props) { + super(props) + alert(props.wocky) + } focus = () => { this.input.focus() @@ -26,7 +36,7 @@ export class FormTextInput extends React.Component { } render() { - const {icon, label, store, imageStyle} = this.props + const {icon, label, value, name, imageStyle} = this.props return ( <> @@ -66,7 +76,7 @@ export class FormTextInput extends React.Component { autoCorrect={false} {...this.props} /> - {Platform.OS === 'android' && !!store && !!store!.value && store!.value.length > 0 && ( + {Platform.OS === 'android' && value.length > 0 && ( { if (store) store.value = '' diff --git a/src/components/MyAccount.tsx b/src/components/MyAccount.tsx index b2e83aed3..865d401ff 100644 --- a/src/components/MyAccount.tsx +++ b/src/components/MyAccount.tsx @@ -16,7 +16,6 @@ import Cell from './Cell' import {FormTextInput} from './FormTextInput' import {colors} from '../constants' import {RText, Separator} from './common' -import {ValidatableProfile} from '../utils/formValidation' import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view' import {IWocky} from 'src/wocky' import {settings} from '../globals' @@ -25,35 +24,18 @@ import Version from './Version' type Props = { wocky?: IWocky - profileValidationStore?: any } -const MyAccount = inject( - 'wocky', - 'profileValidationStore' -)( - observer(({wocky, profileValidationStore}: Props) => { +const MyAccount = inject('wocky')( + observer(({wocky}: Props) => { const {profile} = wocky! - - const [vProfile, setVProfile] = useState(null) const handle = useRef(null) const firstName = useRef(null) const lastName = useRef(null) const email = useRef(null) // const phone = useRef(null) - useEffect(() => { - if (profile) { - const vProf = new ValidatableProfile(profile) - setVProfile(vProf) - profileValidationStore.setProfile(vProf) - - // set the "static" context - ;(MyAccount as any).profileValidationStore = profileValidationStore - } - }, []) - - if (!profile || !vProfile) { + if (!profile) { // error('NULL PROFILE') return } @@ -73,7 +55,7 @@ const MyAccount = inject( firstName.current!.focus()} diff --git a/src/model/Profile.ts b/src/model/Profile.ts index 59bd6c689..6410a17fc 100644 --- a/src/model/Profile.ts +++ b/src/model/Profile.ts @@ -11,6 +11,7 @@ import moment from 'moment' import {Address} from './Address' import {when} from 'mobx' import _ from 'lodash' +import validate from 'validate.js' export enum FriendShareTypeEnum { ALWAYS = 'ALWAYS', @@ -30,6 +31,54 @@ export const FriendShareConfig = types.model({ export interface IFriendShareConfig extends SnapshotIn {} +// eslint-disable-next-line +const isAlphabet = '^\\pL[ 0-9`\'"\u0060\u00B4\u2018\u2019\u201C\u201D\\pL]*[\\pL0-9]?$' + +const profileConstraints = { + handle: { + length: { + minimum: 3, + maximum: 16, + message: 'must be 3 - 16 characters', + }, + format: { + pattern: /\w+/, + message: 'can only contain alphanumeric characters and _', + }, + // this validator set in SearchStore.js + usernameUniqueValidator: true, + }, + firstName: { + format: { + pattern: isAlphabet, + message: 'is invalid', + }, + length: { + minimum: 1, + maximum: 32, + message: 'must be 1 - 32 characters', + }, + }, + lastName: { + format: { + pattern: isAlphabet, + message: 'is invalid', + }, + length: { + minimum: 1, + maximum: 32, + message: 'must be 1 - 32 characters', + }, + }, + email: { + email: true, + length: { + maximum: 254, + message: 'must be less than 254 characters', + }, + }, +} + export const Profile = types .compose( Base, @@ -115,6 +164,7 @@ export const Profile = types })) .extend(self => { const superLoad = self.load + let errors = '' return { actions: { load({avatar, shareType, ownShareType, ...data}: any) { @@ -134,6 +184,13 @@ export const Profile = types self.statusUpdatedAt = data.statusUpdatedAt } }, + validate: flow(function*() { + try { + yield validate.async(self, profileConstraints) + } catch (e) { + errors = e + } + }), invite: flow(function*(shareType: FriendShareTypeEnum = FriendShareTypeEnum.DISABLED) { yield waitFor(() => self.connected) self.receivedInvite() @@ -222,6 +279,12 @@ export const Profile = types }, }, views: { + get errors() { + return errors + }, + get isValid(): boolean { + return !errors + }, get isOwn(): boolean { const ownProfile = self.service && self.service.profile return ownProfile && self.id === ownProfile.id diff --git a/src/store/ProfileValidationStore.ts b/src/store/ProfileValidationStore.ts deleted file mode 100644 index 9b564f89d..000000000 --- a/src/store/ProfileValidationStore.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {types, getParent} from 'mobx-state-tree' -import {ValidatableProfile} from '../utils/formValidation' -import {IWocky} from 'src/wocky' - -const ProfileValidationStore = types.model('ProfileValidationStore', {}).actions(self => { - let vProfile: ValidatableProfile - - const {profile} = (getParent(self) as any).wocky! as IWocky - - function save() { - if (profile) { - return profile!.update(vProfile.asObject) - } - } - - function setProfile(prof: ValidatableProfile) { - vProfile = prof - } - - return {save, setProfile} -}) - -export default ProfileValidationStore diff --git a/src/store/store.ts b/src/store/store.ts index c4d9e6dd8..76dcf9e34 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -17,7 +17,6 @@ import AuthStore from './AuthStore' import fileService from './fileService' import LocationStore from './LocationStore' import SearchStore from './SearchStore' -import ProfileValidationStore from './ProfileValidationStore' import NotificationStore from './NotificationStore' import {PermissionStore} from './PermissionStore' import CodepushStore from './CodePushStore' @@ -54,7 +53,6 @@ export const cleanState = { authStore: {}, locationStore: {}, searchStore: {}, - profileValidationStore: {}, homeStore: {}, navStore: {}, codePushStore: {}, @@ -71,7 +69,6 @@ const Store = types authStore: AuthStore, locationStore: LocationStore, searchStore: SearchStore, - profileValidationStore: ProfileValidationStore, codePushStore: CodepushStore, navStore: NavStore, geocodingStore: GeocodingStore, diff --git a/src/utils/ValidateItem.ts b/src/utils/ValidateItem.ts deleted file mode 100644 index 64b6f3926..000000000 --- a/src/utils/ValidateItem.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {observable, reaction} from 'mobx' - -export class ValidateItem { - @observable errorMessage: string = '' - @observable value: string - @observable isValid?: boolean - key: string - - constructor(key: string, value: string, validator: any) { - this.key = key - this.value = value - reaction( - () => this.value, - val => - validator({[this.key]: val}) - .then(r => { - this.isValid = true - this.errorMessage = '' - }) - .catch(e => { - this.isValid = false - this.errorMessage = e[key][0] - }) - ) - } -} diff --git a/src/utils/formValidation.ts b/src/utils/formValidation.ts deleted file mode 100644 index 31b5bcb24..000000000 --- a/src/utils/formValidation.ts +++ /dev/null @@ -1,95 +0,0 @@ -import {observable, computed} from 'mobx' -import validate from 'validate.js' -import {ValidateItem} from './ValidateItem' -export {ValidateItem} from './ValidateItem' - -// eslint-disable-next-line -const isAlphabet = '^\\pL[ 0-9`\'"\u0060\u00B4\u2018\u2019\u201C\u201D\\pL]*[\\pL0-9]?$' - -const profileConstraints = { - handle: { - length: { - minimum: 3, - maximum: 16, - message: 'must be 3 - 16 characters', - }, - format: { - pattern: /\w+/, - message: 'can only contain alphanumeric characters and _', - }, - // this validator set in SearchStore.js - usernameUniqueValidator: true, - }, - firstName: { - format: { - pattern: isAlphabet, - message: 'is invalid', - }, - length: { - minimum: 1, - maximum: 32, - message: 'must be 1 - 32 characters', - }, - }, - lastName: { - format: { - pattern: isAlphabet, - message: 'is invalid', - }, - length: { - minimum: 1, - maximum: 32, - message: 'must be 1 - 32 characters', - }, - }, - email: { - email: true, - length: { - maximum: 254, - message: 'must be less than 254 characters', - }, - }, -} - -// export for use in tests -export const validateProfile = async (profileObject: any): Promise => { - return new Promise((resolve, reject) => { - validate.async(profileObject, profileConstraints).then(res => resolve(res), res => reject(res)) - }) -} - -type VProfileType = { - handle: string | null - firstName: string | null - lastName: string | null - email: string | null -} - -export class ValidatableProfile { - @observable handle?: ValidateItem - @observable firstName?: ValidateItem - @observable lastName?: ValidateItem - @observable email?: ValidateItem - - constructor(obj: VProfileType) { - Object.keys(obj).forEach(key => { - if (['handle', 'firstName', 'lastName', 'email'].includes(key)) { - this[key] = new ValidateItem(key, obj[key], validateProfile) - } - }) - } - - @computed - get isValid(): boolean { - return !!this.handle!.isValid - } - - get asObject(): any { - return { - handle: this.handle!.value, - firstName: this.firstName!.value, - lastName: this.lastName!.value, - email: this.email!.value, - } - } -}