-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidator.test.js
More file actions
380 lines (312 loc) · 15.1 KB
/
Copy pathvalidator.test.js
File metadata and controls
380 lines (312 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
const mocha = require('mocha')
const chai = require('chai')
chai.should()
const mocks = require('./mocks/typeform-form')
const v = require('./validator')
describe('validator', () => {
it('should validate statement with custom reponseMessage', () => {
const field = { type: 'statement', title: 'foo', ref: 'foo', md: { responseMessage: 'foobarbaz' } }
const res = v.validator(field)({ foo: 'bar' })
res.message.should.equal('foobarbaz')
})
it('should validate when not a mobile', () => {
const field = { type: 'phone_number', title: 'foo', ref: 'foo', md: { validate: { country: 'IN', mobile: true } } }
const res = v.validator(field)('+914888000000')
res.valid.should.equal(false)
res.message.should.equal('Sorry, please enter a valid phone number.')
})
it('should validate when not a phone number', () => {
const field = { type: 'phone_number', title: 'foo', ref: 'foo', md: { validate: { country: 'IN', mobile: false } } }
const res = v.validator(field)('9999')
res.valid.should.equal(false)
res.message.should.equal('Sorry, please enter a valid phone number.')
})
it('should validate when a mobile number', () => {
const field = { type: 'phone_number', title: 'foo', ref: 'foo', md: { validate: { country: 'IN', mobile: true } } }
let res = v.validator(field)('+918888000000')
res.valid.should.equal(true)
res = v.validator(field)(8888000000)
res.valid.should.equal(true)
})
it('should validate upload types - when correct type', () => {
const field = { type: 'upload', title: 'foo', ref: 'foo', md: { upload: { type: 'image' } } }
const attachment = { "type": "image", "payload": { "url": "https://scontent.xx.fbcdn.net/v/t1.15752-9/461148037_759263159639423_7161323123727879546_n.jpg?_nc_cat=102&ccb=1-7&_nc_sid=fc17b8&_nc_ohc=zkDCMxo0pTsQ7kNvgGA_H8d&_nc_ad=z-m&_nc_cid=0&_nc_ht=scontent.xx&_nc_gid=AEZgev0WN3sV8E56pu3IELa&oh=03_Q7cD1QHpWUMM_ryYpocqe5jG_MF5bg12hw79eHeTmvbg8jVNHg&oe=67222F34" } }
const res = v.validator(field)(attachment)
res.valid.should.equal(true)
})
it('should invalidate upload types - when incorrect type', () => {
const field = { type: 'upload', title: 'foo', ref: 'foo', md: { upload: { type: 'video' } } }
const attachment = { "type": "image", "payload": { "url": "https://scontent.xx.fbcdn.net/v/t1.15752-9/461148037_759263159639423_7161323123727879546_n.jpg?_nc_cat=102&ccb=1-7&_nc_sid=fc17b8&_nc_ohc=zkDCMxo0pTsQ7kNvgGA_H8d&_nc_ad=z-m&_nc_cid=0&_nc_ht=scontent.xx&_nc_gid=AEZgev0WN3sV8E56pu3IELa&oh=03_Q7cD1QHpWUMM_ryYpocqe5jG_MF5bg12hw79eHeTmvbg8jVNHg&oe=67222F34" } }
const res = v.validator(field)(attachment)
res.valid.should.equal(false)
})
it('should invalidate upload types - when attachment undefined', () => {
const field = { type: 'upload', title: 'foo', ref: 'foo', md: { upload: { type: 'video' } } }
const attachment = undefined
const res = v.validator(field)(attachment)
res.valid.should.equal(false)
})
// parseNumber tests - data-driven
describe('parseNumber', () => {
// [input, locale, expected]
const testCases = [
// US format (default) - integers
['123', null, 123],
['0', null, 0],
['-456', null, -456],
['+789', null, 789],
// US format - decimals
['100.50', null, 100.5],
['0.99', null, 0.99],
['-123.45', null, -123.45],
['3.14159', null, 3.14159],
// US format - thousands separators
['1,234', null, 1234],
['1,234.56', null, 1234.56],
['1,234,567.89', null, 1234567.89],
['10,000,000', null, 10000000],
// US format - edge cases
['.5', null, 0.5],
['-.5', null, -0.5],
['100.', null, 100],
[' 123 ', null, 123],
// European format (de-DE) - comma decimal
['100,50', 'de-DE', 100.5],
['3,14159', 'de-DE', 3.14159],
['-0,99', 'de-DE', -0.99],
// European format - dot thousands
['1.234', 'de-DE', 1234],
['1.234,56', 'de-DE', 1234.56],
['1.234.567,89', 'de-DE', 1234567.89],
// Arabic Saudi (ar-SA) - Arabic-Indic numerals
['١٢٣', 'ar-SA', 123],
['٩٨٧٦٥', 'ar-SA', 98765],
['٠', 'ar-SA', 0],
// Arabic Saudi - native separators (٬ thousands, ٫ decimal)
['١٬٢٣٤', 'ar-SA', 1234],
['١٬٢٣٤٫٥٦', 'ar-SA', 1234.56],
// Arabic Saudi - Arabic digits with ASCII separators (common with ASCII keyboard)
['١,٢٣٤.٥٦', 'ar-SA', 1234.56],
['١٢٣.٤٥', 'ar-SA', 123.45],
// Arabic Morocco (ar-MA) - French style (space thousands, comma decimal)
['1 234,56', 'ar-MA', 1234.56],
['1 234 567,89', 'ar-MA', 1234567.89],
['100,50', 'ar-MA', 100.5],
// Unicode - Devanagari (Hindi)
['१२३', null, 123],
['१००.५०', null, 100.5],
['१,२३४.५६', null, 1234.56],
// Unicode - Persian/Farsi
['۱۲۳۴۵', null, 12345],
['۱۲۳.۴۵', null, 123.45],
// Unicode - other scripts
['৫৬৭', null, 567], // Bengali
['௧௨௩', null, 123], // Tamil
['๑๒๓๔๕', null, 12345], // Thai
// Pass-through numbers
[123.45, null, 123.45],
[0, null, 0],
[-42, null, -42],
// Cross-locale: ar-SA locale but US-style ASCII input
// WORKS because ar-SA separators (٬ thousands, ٫ decimal) normalize to US-style (, and .)
['100.50', 'ar-SA', 100.5],
['1,234.56', 'ar-SA', 1234.56],
// Cross-locale: ar-MA locale but US-style ASCII input
// BROKEN - ar-MA is European-style (comma=decimal), US input has dot=decimal
// This is a locale mismatch - if you specify ar-MA, use European format
['100.50', 'ar-MA', 10050], // dot not recognized as decimal, gets stripped weirdly
['1,234.56', 'ar-MA', 1.23456], // comma becomes decimal, dot breaks parsing
// Cross-locale: en-US/default locale but Arabic script input
// WORKS because Arabic digits/separators normalize to US-style ASCII
['١٠٠.٥٠', null, 100.5], // Arabic digits, ASCII decimal
['١٬٢٣٤٫٥٦', null, 1234.56], // Arabic digits + Arabic separators → US-style
['١٢٣', null, 123], // Plain Arabic digits
// Invalid inputs → null
[true, null, null],
[false, null, null],
['abc', null, null],
['12abc', null, null],
['8888 mil', null, null],
['', null, null],
['1.2.3', null, null],
['1..2', null, null],
]
testCases.forEach(([input, locale, expected]) => {
const localeStr = locale || 'default'
const inputStr = typeof input === 'string' ? `"${input}"` : input
it(`parseNumber(${inputStr}, ${localeStr}) → ${expected}`, () => {
const result = locale ? v.parseNumber(input, locale) : v.parseNumber(input)
if (expected === null) {
chai.expect(result).to.be.null
} else {
result.should.equal(expected)
}
})
})
})
// Minimal validation test (validation is just parseNumber !== null)
it('validates numbers via field type', () => {
const field = { type: 'number', title: 'foo', ref: 'foo' }
v.validator(field)('123').valid.should.equal(true)
v.validator(field)('abc').valid.should.equal(false)
v.validator(field)(true).valid.should.equal(false)
})
it('validates numbers using locale from field.md', () => {
// European format: dot=thousands, comma=decimal
const deField = { type: 'number', title: 'foo', ref: 'foo', md: { locale: 'de-DE' } }
// European format should be valid with de-DE locale
v.validator(deField)('1.234,56').valid.should.equal(true)
v.validator(deField)('100,50').valid.should.equal(true)
// Default (no locale) uses US format
const usField = { type: 'number', title: 'foo', ref: 'foo' }
v.validator(usField)('1,234.56').valid.should.equal(true)
// European format with multiple dots is invalid for US locale
v.validator(usField)('1.234.567,89').valid.should.equal(false)
})
it('validates Arabic numerals using ar-SA locale from field.md', () => {
const field = { type: 'number', title: 'foo', ref: 'foo', md: { locale: 'ar-SA' } }
// Arabic-Indic numerals with Arabic separators
v.validator(field)('١٬٢٣٤٫٥٦').valid.should.equal(true)
v.validator(field)('١٢٣').valid.should.equal(true)
// ASCII with ar-SA also works (Arabic separators normalize to US-style)
v.validator(field)('1,234.56').valid.should.equal(true)
})
describe('validateLegalYesNo', () => {
// legal field - new QR format with string values
it('validates legal field with string response (new QR format)', () => {
const field = { type: 'legal', title: 'Do you accept?', ref: 'foo' }
v.validator(field)('I Accept').valid.should.equal(true)
v.validator(field)("I don't Accept").valid.should.equal(true)
v.validator(field)('Invalid').valid.should.equal(false)
})
// legal field - old postback format with boolean values
it('validates legal field with boolean response (old postback format)', () => {
const field = { type: 'legal', title: 'Do you accept?', ref: 'foo' }
v.validator(field)(true).valid.should.equal(true) // maps to first option 'I Accept'
v.validator(field)(false).valid.should.equal(true) // maps to second option "I don't Accept"
})
// legal field - full postback payload object
it('validates legal field with postback payload object', () => {
const field = { type: 'legal', title: 'Do you accept?', ref: 'foo' }
// Old postback sends full payload object with boolean value
v.validator(field)({ value: true, ref: 'foo' }).valid.should.equal(true)
v.validator(field)({ value: false, ref: 'foo' }).valid.should.equal(true)
// New QR sends full payload object with string value
v.validator(field)({ value: 'I Accept', ref: 'foo' }).valid.should.equal(true)
})
// yes_no field - new QR format with string values
it('validates yes_no field with string response (new QR format)', () => {
const field = { type: 'yes_no', title: 'Is this correct?', ref: 'bar' }
v.validator(field)('Yes').valid.should.equal(true)
v.validator(field)('No').valid.should.equal(true)
v.validator(field)('Maybe').valid.should.equal(false)
})
// yes_no field - old postback format with boolean values
it('validates yes_no field with boolean response (old postback format)', () => {
const field = { type: 'yes_no', title: 'Is this correct?', ref: 'bar' }
v.validator(field)(true).valid.should.equal(true) // maps to first option 'Yes'
v.validator(field)(false).valid.should.equal(true) // maps to second option 'No'
})
// yes_no field - full postback payload object
it('validates yes_no field with postback payload object', () => {
const field = { type: 'yes_no', title: 'Is this correct?', ref: 'bar' }
v.validator(field)({ value: true, ref: 'bar' }).valid.should.equal(true)
v.validator(field)({ value: false, ref: 'bar' }).valid.should.equal(true)
v.validator(field)({ value: 'Yes', ref: 'bar' }).valid.should.equal(true)
})
// error message check
it('returns correct error message for invalid response', () => {
const field = { type: 'legal', title: 'Do you accept?', ref: 'foo' }
const res = v.validator(field)('Invalid')
res.valid.should.equal(false)
res.message.should.equal('Sorry, please use the buttons provided to answer the question.')
})
})
describe('validateButtonChoice', () => {
const field = {
type: 'button_choice',
title: 'Pick an option',
ref: 'button-ref',
properties: {
choices: [
{ id: '1', ref: 'a', label: 'Option A' },
{ id: '2', ref: 'b', label: 'Option B' },
{ id: '3', ref: 'c', label: 'Option C' }
]
}
}
it('validates string response matching button label', () => {
v.validator(field)('Option A').valid.should.equal(true)
v.validator(field)('Option B').valid.should.equal(true)
v.validator(field)('Option C').valid.should.equal(true)
})
it('invalidates string response not matching any button', () => {
v.validator(field)('Option D').valid.should.equal(false)
v.validator(field)('Invalid').valid.should.equal(false)
})
it('validates postback payload object with value property', () => {
// Postback sends payload object with {value, ref}
v.validator(field)({ value: 'Option A', ref: 'button-ref' }).valid.should.equal(true)
v.validator(field)({ value: 'Option B', ref: 'button-ref' }).valid.should.equal(true)
})
it('invalidates postback payload with invalid value', () => {
v.validator(field)({ value: 'Invalid', ref: 'button-ref' }).valid.should.equal(false)
})
it('returns correct error message for invalid response', () => {
const res = v.validator(field)('Invalid')
res.valid.should.equal(false)
res.message.should.equal('Sorry, please use the buttons provided to answer the question.')
})
})
describe('validateUtilityMessage', () => {
// The approved Messenger utility template bakes in `value == button_label`
// for each POSTBACK button, so the set of valid response values is the
// labels of properties.choices — same source the translator reads to emit
// the buttons component.
const field = {
type: 'utility_message',
title: 'Survey results',
ref: 'utility-ref',
properties: {
choices: [
{ id: '1', ref: 'a', label: 'yes' },
{ id: '2', ref: 'b', label: 'no' }
]
},
md: {
template: 'results_ready',
language: 'en_US',
params: ['Alice']
}
}
it('is registered in the dispatch table (no longer throws)', () => {
;(() => v.validator(field)).should.not.throw()
})
it('validates string response matching a choice label', () => {
v.validator(field)('yes').valid.should.equal(true)
v.validator(field)('no').valid.should.equal(true)
})
it('invalidates string response not matching any choice', () => {
v.validator(field)('maybe').valid.should.equal(false)
})
it('validates postback payload object with value property', () => {
v.validator(field)({ value: 'yes', ref: 'utility-ref' }).valid.should.equal(true)
v.validator(field)({ value: 'no', ref: 'utility-ref' }).valid.should.equal(true)
})
it('invalidates postback payload with invalid value', () => {
v.validator(field)({ value: 'maybe', ref: 'utility-ref' }).valid.should.equal(false)
})
it('returns the buttons-required error for an invalid response', () => {
const res = v.validator(field)('Invalid')
res.valid.should.equal(false)
res.message.should.equal('Sorry, please use the buttons provided to answer the question.')
})
it('falls back to statement-style behavior for text-only templates (no choices)', () => {
// A utility_message with no buttons accepts no reply — model it as a
// statement (always invalid), so the error path is consistent with
// other statement-style fields.
const textOnly = { ...field, properties: {} }
v.validator(textOnly)('any').valid.should.equal(false)
})
})
})