Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sms limiting #2504

Merged
merged 17 commits into from
Aug 11, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8f281ba
feat(sms-limiting): email for disabling sms verification (#2133)
seaerchin Jun 14, 2021
c92a35f
feat(sms limit): sms warning for different limits (#2104)
seaerchin Jun 14, 2021
77aa155
feat(verified-sms): disabling sms verifications (#2262)
seaerchin Jun 28, 2021
998f23a
feat(sms-limiting): admin facing frontend (#2280)
seaerchin Jul 14, 2021
bd9216d
feat(sms-limiting): disable sms verifications toggling (#2300)
seaerchin Jul 26, 2021
c552a92
feat(sms-limiting): check admin sms count on public submission (#2326)
seaerchin Jul 28, 2021
73ae661
refactor(configure-mobile.client.directive): updated to account for n…
seaerchin Aug 2, 2021
164b004
chore(types): clean up unused types
seaerchin Aug 3, 2021
b023f93
Merge branch 'develop' into feat/sms-limiting
seaerchin Aug 4, 2021
82aa6ed
test(verification): fixed import path in tests
seaerchin Aug 4, 2021
2240016
fix(mail.service): updated to cc rather than send individual mail
seaerchin Aug 4, 2021
c2736ec
chore(create-isonboardedacc): updated db script to be more specific
seaerchin Aug 7, 2021
5842cc0
fix(sms_count.server.model): updated read pref to secondary
seaerchin Aug 7, 2021
3a46c33
fix(form.server.model): updated disable to only affect forms w/o msg …
seaerchin Aug 10, 2021
28f6e85
test(form.server.model): updated tests to check that only not onboard…
seaerchin Aug 10, 2021
f4b0a6c
feat(sms-limiting): ui changes for modal (#2541)
seaerchin Aug 10, 2021
7242a37
feat(sms-limiting): disabled email (#2532)
seaerchin Aug 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions scripts/20210628_create-isOnboardedAcc/create-isOnboardedAcc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* eslint-disable */

/*
This script creates a new key, isOnboardedAccount, on the smscounts db which indexes the msgSrvcId variable
*/

let formTwilioId = 'insert the form twilio id here'

// == PRE-UPDATE CHECKS ==
// Count the number of verifications (A)
db.getCollection('smscounts').count({ smsType: 'VERIFICATION' })

// Count of forms using our twilio acc (B)
db.getCollection('smscounts').count({
smsType: 'VERIFICATION',
msgSrvcSid: {$eq: formTwilioId},
})

// Count of forms using their own twilio acc (C)
// A === B + C
db.getCollection('smscounts').count({
smsType: 'VERIFICATION',
msgSrvcSid: { $ne: formTwilioId },
})


// == UPDATE ==
// Update verifications which have message service id equal to form twilio id
db.getCollection('smscounts').updateMany(
{ smsType: 'VERIFICATION', msgSrvcSid: { $eq: formTwilioId } },
{
$set: {
isOnboardedAccount: false,
},
}
)

// Update verifications whose message service id is not equal to form twilio id
db.getCollection('smscounts').updateMany(
{ smsType: 'VERIFICATION', msgSrvcSid: { $ne: formTwilioId } },
{
$set: {
isOnboardedAccount: true,
},
}
)

// == POST-UPDATE CHECKS ==
// Check number of verifications updated
// Sum of these two should be equal to the initial count
// Count of forms using our twilio acc
db.getCollection('smscounts').count({
smsType: 'VERIFICATION',
msgSrvcSid: { $eq: formTwilioId },
isOnboardedAccount: false
})

// Count of forms using their own twilio acc
db.getCollection('smscounts').count({
smsType: 'VERIFICATION',
msgSrvcSid: { $ne: formTwilioId },
isOnboardedAccount: true
})
30 changes: 30 additions & 0 deletions scripts/20210628_create-isOnboardedAcc/delete-isOnboardedAcc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable */

/*
This script creates a new key, isOnboardedAccount, on the smscounts db which indexes the msgSrvcId variable
*/

// == PRE-UPDATE CHECKS ==
// Count the number of verifications we have to update
db.getCollection('smscounts').count({ smsType: 'VERIFICATION' })

// == UPDATE ==
// Update verified smses
db.getCollection('smscounts').updateMany(
{ smsType: 'VERIFICATION' },
{
$unset: {
isOnboardedAccount: false,
},
}
)

// == POST-UPDATE CHECKS ==
// Check number of verifications updated
// SHOULD BE 0
db.getCollection('smscounts').count({
smsType: 'VERIFICATION',
isOnboardedAccount: {
$exists: true
}
})
12 changes: 12 additions & 0 deletions shared/utils/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,15 @@ export enum VfnErrors {
TransactionNotFound = 'TRANSACTION_NOT_FOUND',
InvalidMobileNumber = 'INVALID_MOBILE_NUMBER',
}

export enum ADMIN_VERIFIED_SMS_STATES {
limitExceeded = 'LIMIT_EXCEEDED',
belowLimit = 'BELOW_LIMIT',
hasMessageServiceId = 'MESSAGE_SERVICE_ID_OBTAINED',
}

export enum SMS_WARNING_TIERS {
LOW = 2500,
MED = 5000,
HIGH = 7500,
}
127 changes: 126 additions & 1 deletion src/app/models/__tests__/form.server.model.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { ObjectId } from 'bson-ext'
import { cloneDeep, map, merge, omit, orderBy, pick } from 'lodash'
import { cloneDeep, map, merge, omit, orderBy, pick, range } from 'lodash'
import mongoose, { Types } from 'mongoose'
import {
EMAIL_PUBLIC_FORM_FIELDS,
Expand Down Expand Up @@ -1802,6 +1802,131 @@ describe('Form Model', () => {
await expect(Form.countDocuments()).resolves.toEqual(0)
})
})

describe('disableSmsVerificationsForUser', () => {
it('should disable sms verifications for all forms belonging to a user successfully', async () => {
// Arrange
const mockFormPromises = range(3).map(() => {
return Form.create({
admin: populatedAdmin._id,
responseMode: ResponseMode.Email,
title: 'mock mobile form',
emails: [populatedAdmin.email],
form_fields: [
generateDefaultField(BasicField.Mobile, { isVerifiable: true }),
],
})
})
await Promise.all(mockFormPromises)

// Act
await Form.disableSmsVerificationsForUser(populatedAdmin._id)

// Assert
// Find all forms that match admin id
const updatedForms = await Form.find({ admin: populatedAdmin._id })
updatedForms.map(({ form_fields }) =>
form_fields!.map((field) => {
expect(field.isVerifiable).toBe(false)
}),
)
})

it('should not disable non mobile fields for a user', async () => {
// Arrange
const mockFormPromises = range(3).map(() => {
return Form.create({
admin: populatedAdmin._id,
responseMode: ResponseMode.Email,
title: 'mock email form',
emails: [populatedAdmin.email],
form_fields: [
generateDefaultField(BasicField.Email, { isVerifiable: true }),
],
})
})
await Promise.all(mockFormPromises)

// Act
await Form.disableSmsVerificationsForUser(populatedAdmin._id)

// Assert
// Find all forms that match admin id
const updatedForms = await Form.find({ admin: populatedAdmin._id })
updatedForms.map(({ form_fields }) =>
form_fields!.map((field) => {
expect(field.isVerifiable).toBe(true)
}),
)
})

it('should only disable sms verifications for a particular user', async () => {
// Arrange
const MOCK_USER_ID = new ObjectId()
await dbHandler.insertFormCollectionReqs({
userId: MOCK_USER_ID,
mailDomain: 'something.com',
})
await Form.create({
admin: populatedAdmin._id,
responseMode: ResponseMode.Email,
title: 'mock email form',
emails: [populatedAdmin.email],
form_fields: [
generateDefaultField(BasicField.Mobile, { isVerifiable: true }),
],
})
await Form.create({
admin: MOCK_USER_ID,
responseMode: ResponseMode.Email,
title: 'mock email form',
emails: [populatedAdmin.email],
form_fields: [
generateDefaultField(BasicField.Email, { isVerifiable: true }),
],
})

// Act
await Form.disableSmsVerificationsForUser(populatedAdmin._id)

// Assert
// Find all forms that match admin id
const updatedMobileForm = await Form.find({ admin: populatedAdmin._id })
expect(updatedMobileForm[0]!.form_fields[0].isVerifiable).toBe(false)
const updatedEmailForm = await Form.find({ admin: MOCK_USER_ID })
expect(updatedEmailForm[0]!.form_fields[0].isVerifiable).toBe(true)
})

it('should not update when a db error occurs', async () => {
// Arrange
const disableSpy = jest.spyOn(Form, 'disableSmsVerificationsForUser')
disableSpy.mockResolvedValueOnce(new Error('tee hee db crashed'))
const mockFormPromises = range(3).map(() => {
return Form.create({
admin: populatedAdmin._id,
responseMode: ResponseMode.Email,
title: 'mock mobile form',
emails: [populatedAdmin.email],
form_fields: [
generateDefaultField(BasicField.Mobile, { isVerifiable: true }),
],
})
})
await Promise.all(mockFormPromises)

// Act
await Form.disableSmsVerificationsForUser(populatedAdmin._id)

// Assert
// Find all forms that match admin id
const updatedForms = await Form.find({ admin: populatedAdmin._id })
updatedForms.map(({ form_fields }) =>
form_fields!.map((field) => {
expect(field.isVerifiable).toBe(true)
}),
)
})
})
})

describe('Methods', () => {
Expand Down
25 changes: 25 additions & 0 deletions src/app/models/form.server.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,31 @@ const compileFormModel = (db: Mongoose): IFormModel => {
).exec()
}

FormSchema.statics.disableSmsVerificationsForUser = async function (
userId: IUserSchema['_id'],
) {
return this.updateMany(
// Filter the collection so that only specified user is selected
// Only update forms without message service name
// As it implies that those forms are using default (our) credentials
{
admin: userId,
msgSrvcName: {
$exists: false,
},
},
// Next, set the isVerifiable property for each field in form_fields
// Refer here for $[identifier] syntax: https://docs.mongodb.com/manual/reference/operator/update/positional-filtered/
{ $set: { 'form_fields.$[field].isVerifiable': false } },
{
// Only set if the field has fieldType equal to mobile
arrayFilters: [{ 'field.fieldType': 'mobile' }],
// NOTE: Not updating the timestamp because we should preserve ordering due to user-level modifications
timestamps: false,
},
).exec()
}

// Hooks
FormSchema.pre<IFormSchema>('validate', function (next) {
// Reject save if form document is too large
Expand Down
Loading