Verified Commit 4dfdfc6a authored by Mari's avatar Mari Committed by aguestuser
Browse files

[215] add recycleablePhoneNumber model/repository

parent 14eb1261
......@@ -69,7 +69,7 @@ const routesOf = async router => {
router.post('/phoneNumbers/recycle', async ctx => {
const { phoneNumbers } = ctx.request.body
const result = await phoneNumberService.recycle({
const result = await phoneNumberService.enqueueRecycleablePhoneNumber({
phoneNumbers,
})
merge(ctx, { status: httpStatusOfMany(result), body: result })
......
......@@ -10,6 +10,8 @@ const { membershipOf } = require('./models/membership')
const { messageCountOf } = require('./models/messageCount')
const { phoneNumberOf } = require('./models/phoneNumber')
const { smsSenderOf } = require('./models/smsSender')
const { recycleablePhoneNumberOf } = require('./models/recycleablePhoneNumber')
const { wait } = require('../util')
const { maxConnectionAttempts, connectionInterval } = config
......@@ -28,6 +30,7 @@ const run = async () => {
membership: membershipOf(sequelize, Sequelize),
messageCount: messageCountOf(sequelize, Sequelize),
phoneNumber: phoneNumberOf(sequelize, Sequelize),
recycleablePhoneNumber: recycleablePhoneNumberOf(sequelize, Sequelize),
smsSender: smsSenderOf(sequelize, Sequelize),
}
......
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('recycleablePhoneNumbers', {
channelPhoneNumber: {
type: Sequelize.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
whenEnqueued: {
type: Sequelize.DATE,
allowNull: false,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW,
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW,
},
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('recycleablePhoneNumbers');
}
};
const recycleablePhoneNumberOf = (sequelize, DataTypes) => {
const recycleablePhoneNumber = sequelize.define('recycleablePhoneNumber', {
channelPhoneNumber: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
unique: true,
},
whenEnqueued: {
type: DataTypes.DATE,
allowNull: false,
},
})
return recycleablePhoneNumber
}
module.exports = { recycleablePhoneNumberOf }
const app = require('../..')
const enqueue = channelPhoneNumber =>
app.db.recycleablePhoneNumber.create({
channelPhoneNumber,
whenEnqueued: new Date().toISOString(),
})
const dequeue = channelPhoneNumber =>
app.db.recycleablePhoneNumber.destroy({ where: { channelPhoneNumber } })
module.exports = { enqueue, dequeue }
......@@ -4,7 +4,7 @@ const { errors } = require('./common')
const { destroy } = require('./destroy')
const { list } = require('./present')
const { provisionN } = require('./provision')
const { recycle } = require('./recycle')
const { enqueueRecycleablePhoneNumber, recycle } = require('./recycle')
const { register, registerAllPurchased, registerAllUnregistered } = require('./register')
const { handleSms } = require('./sms')
const { purchase, purchaseN } = require('./purchase')
......@@ -22,6 +22,7 @@ module.exports = {
provisionN,
purchase,
purchaseN,
enqueueRecycleablePhoneNumber,
recycle,
register,
registerAllPurchased,
......
const channelRepository = require('../../db/repositories/channel')
const phoneNumberRepository = require('../../db/repositories/phoneNumber')
const recycleablePhoneNumberRepository = require('../../db/repositories/recycleablePhoneNumber')
const eventRepository = require('../../db/repositories/event')
const common = require('./common')
const { defaultLanguage } = require('../../config')
......@@ -8,30 +9,39 @@ const { eventTypes } = require('../../db/models/event')
const { sdMessageOf } = require('../../signal/constants')
const { messagesIn } = require('../../dispatcher/strings/messages')
// ({Database, Socket, string}) -> SignalboostStatus
const recycle = async ({ phoneNumbers }) => {
// ({ string }) -> SignalboostStatus
const enqueueRecycleablePhoneNumber = async ({ phoneNumbers }) => {
return await Promise.all(
phoneNumbers.split(',').map(async phoneNumber => {
const channel = await channelRepository.findDeep(phoneNumber)
if (channel) {
return notifyMembers(channel)
.then(() => common.destroyChannel(channel))
.then(() => eventRepository.log(eventTypes.CHANNEL_DESTROYED, phoneNumber))
.then(() => recordStatusChange(phoneNumber, common.statuses.VERIFIED))
.then(phoneNumberStatus => ({ status: 'SUCCESS', data: phoneNumberStatus }))
.catch(err => handleRecycleFailure(err, phoneNumber))
} else {
try {
const channel = await channelRepository.find(phoneNumber)
return recycleablePhoneNumberRepository.enqueue(channel.phoneNumber)
} catch (e) {
return { status: 'ERROR', message: `Channel not found for ${phoneNumber}` }
}
}),
)
}
// (string) -> SignalboostStatus
const recycle = async channelPhoneNumber => {
const channel = await channelRepository.findDeep(channelPhoneNumber)
if (channel) {
return notifyMembers(channel)
.then(() => common.destroyChannel(channel))
.then(() => eventRepository.log(eventTypes.CHANNEL_DESTROYED, channelPhoneNumber))
.then(() => recordStatusChange(channelPhoneNumber, common.statuses.VERIFIED))
.then(phoneNumberStatus => ({ status: 'SUCCESS', data: phoneNumberStatus }))
.catch(err => handleRecycleFailure(err, channelPhoneNumber))
} else {
return { status: 'ERROR', message: `Channel not found for ${channelPhoneNumber}` }
}
}
/********************
* HELPER FUNCTIONS
********************/
// (Database, Socket, Channel) -> Channel
// (Channel) -> Channel
const notifyMembers = async channel => {
const memberPhoneNumbers = channelRepository.getMemberPhoneNumbers(channel)
await signal.broadcastMessage(
......@@ -58,4 +68,4 @@ const handleRecycleFailure = async (err, phoneNumber) => {
}
}
module.exports = { recycle }
module.exports = { enqueueRecycleablePhoneNumber, recycle }
import { expect } from 'chai'
import { describe, it, before, after } from 'mocha'
import { run } from '../../../../app/db/index'
import { genPhoneNumber } from '../../../support/factories/phoneNumber'
describe('recycleablePhoneNumber model', () => {
let db
let channelPhoneNumber = genPhoneNumber()
before(async () => {
db = await run()
})
after(async () => {
await db.recycleablePhoneNumber.destroy({ where: {} })
await db.stop()
})
it('has the correct fields', async () => {
const enqueuedChannelNumber = await db.recycleablePhoneNumber.create({
channelPhoneNumber,
whenEnqueued: new Date().toISOString(),
})
expect(enqueuedChannelNumber.channelPhoneNumber).to.be.a('string')
expect(enqueuedChannelNumber.whenEnqueued).to.be.a('Date')
})
describe('validations', () => {
it('requires a channelPhoneNumber', async () => {
const err = await db.recycleablePhoneNumber
.create({ whenEnqueued: new Date().toISOString() })
.catch(e => e)
expect(err.message).to.include('channelPhoneNumber cannot be null')
})
it('requires a timestamp for whenEnqueued', async () => {
const err = await db.recycleablePhoneNumber
.create({ channelPhoneNumber: genPhoneNumber() })
.catch(e => e)
expect(err.message).to.include('whenEnqueued cannot be null')
})
it("doesn't allow the same phone number to be enqueued twice", async () => {
const err = await db.recycleablePhoneNumber
.create({
channelPhoneNumber,
whenEnqueued: new Date().toISOString(),
})
.catch(e => JSON.stringify(e.errors[0]))
expect(err).to.include('channelPhoneNumber must be unique')
})
})
})
import { describe, it, before, after, beforeEach } from 'mocha'
import { expect } from 'chai'
import { genPhoneNumber } from '../../../support/factories/phoneNumber'
import recycleablePhoneNumberRepository from '../../../../app/db/repositories/recycleablePhoneNumber'
import app from '../../../../app'
import testApp from '../../../support/testApp'
import dbService from '../../../../app/db'
describe('recycleablePhoneNumber repository', () => {
let db, recycleablePhoneNumberCount, channelPhoneNumber
before(async () => {
db = (await app.run({ ...testApp, db: dbService })).db
channelPhoneNumber = genPhoneNumber()
})
beforeEach(async () => {
recycleablePhoneNumberCount = await db.recycleablePhoneNumber.count()
})
after(async () => await app.stop())
it('enqueues a channel for recycling', async () => {
let enqueuedChannel = await recycleablePhoneNumberRepository.enqueue(channelPhoneNumber)
expect(enqueuedChannel.channelPhoneNumber).to.eql(channelPhoneNumber)
expect(await db.recycleablePhoneNumber.count()).to.eql(recycleablePhoneNumberCount + 1)
})
it('dequeues a channel for recycling', async () => {
await recycleablePhoneNumberRepository.dequeue(channelPhoneNumber)
expect(await db.recycleablePhoneNumber.count()).to.eql(recycleablePhoneNumberCount - 1)
})
})
import { expect } from 'chai'
import { afterEach, beforeEach, describe, it } from 'mocha'
import phoneNumberService from '../../../../app/registrar/phoneNumber'
import { enqueueRecycleablePhoneNumber, recycle } from '../../../../app/registrar/phoneNumber'
import sinon from 'sinon'
import phoneNumberRepository from '../../../../app/db/repositories/phoneNumber'
import channelRepository from '../../../../app/db/repositories/channel'
......@@ -10,7 +10,7 @@ import common from '../../../../app/registrar/phoneNumber/common'
import { eventTypes } from '../../../../app/db/models/event'
describe('phone number services -- recycle module', () => {
const phoneNumbers = '+11111111111,+12222222222'
const phoneNumbers = ['+11111111111', '+12222222222']
let updatePhoneNumberStub,
broadcastMessageStub,
findChannelStub,
......@@ -68,27 +68,21 @@ describe('phone number services -- recycle module', () => {
broadcastMessageStub.callsFake(() => Promise.reject('Failed to broadcast message'))
describe('recycling phone numbers', () => {
describe('when phone numbers do not exist in channels db', () => {
describe('when the phone number does not belong to a valid channel', () => {
beforeEach(async () => {
findChannelStub.returns(Promise.resolve(null))
})
it('returns a channel not found status', async () => {
const response = await phoneNumberService.recycle({ phoneNumbers })
expect(response).to.eql([
{
message: 'Channel not found for +11111111111',
status: 'ERROR',
},
{
message: 'Channel not found for +12222222222',
status: 'ERROR',
},
])
const response = await recycle(phoneNumbers[0])
expect(response).to.eql({
message: 'Channel not found for +11111111111',
status: 'ERROR',
})
})
})
describe('when phone numbers do exist in channels db', () => {
describe('when the phone number does belong to a valid channel', () => {
beforeEach(async () => {
findChannelStub.returns(Promise.resolve({}))
})
......@@ -104,20 +98,13 @@ describe('phone number services -- recycle module', () => {
})
it('returns a failed status', async () => {
const response = await phoneNumberService.recycle({ phoneNumbers })
expect(response).to.eql([
{
message:
'Failed to recycle channel for +11111111111. Error: Failed to broadcast message',
status: 'ERROR',
},
{
message:
'Failed to recycle channel for +12222222222. Error: Failed to broadcast message',
status: 'ERROR',
},
])
const response = await recycle(phoneNumbers[0])
expect(response).to.eql({
message:
'Failed to recycle channel for +11111111111. Error: Failed to broadcast message',
status: 'ERROR',
})
})
})
......@@ -137,9 +124,9 @@ describe('phone number services -- recycle module', () => {
})
it('notifies the members of the channel of destruction', async () => {
await phoneNumberService.recycle({ phoneNumbers })
await recycle(phoneNumbers[0])
expect(broadcastMessageStub.callCount).to.eql(2)
expect(broadcastMessageStub.callCount).to.eql(1)
})
it('adds a CHANNEL_DESTROYED event to the event log', async () => {
......@@ -152,7 +139,7 @@ describe('phone number services -- recycle module', () => {
})
it('updates the phone number record to verified', async () => {
await phoneNumberService.recycle({ phoneNumbers })
await recycle(phoneNumbers[0])
expect(updatePhoneNumberStub.getCall(0).args).to.eql([
'+11111111111',
......@@ -161,30 +148,21 @@ describe('phone number services -- recycle module', () => {
})
it('successfully destroys the channel', async () => {
await phoneNumberService.recycle({ phoneNumbers })
await recycle(phoneNumbers[0])
expect(destroyChannelSpy.callCount).to.eql(2)
expect(destroyChannelSpy.callCount).to.eql(1)
})
it('returns successful recycled phone number statuses', async () => {
const response = await phoneNumberService.recycle({ phoneNumbers })
expect(response).to.eql([
{
data: {
phoneNumber: '+11111111111',
status: 'VERIFIED',
},
status: 'SUCCESS',
},
{
data: {
phoneNumber: '+12222222222',
status: 'VERIFIED',
},
status: 'SUCCESS',
const response = await recycle(phoneNumbers[0])
expect(response).to.eql({
data: {
phoneNumber: '+11111111111',
status: 'VERIFIED',
},
])
status: 'SUCCESS',
})
})
})
......@@ -194,20 +172,13 @@ describe('phone number services -- recycle module', () => {
})
it('returns a failed status', async () => {
const response = await phoneNumberService.recycle({ phoneNumbers })
const response = await recycle(phoneNumbers[0])
expect(response).to.eql([
{
message:
'Failed to recycle channel for +11111111111. Error: DB phoneNumber update failure',
status: 'ERROR',
},
{
message:
'Failed to recycle channel for +12222222222. Error: DB phoneNumber update failure',
status: 'ERROR',
},
])
expect(response).to.eql({
message:
'Failed to recycle channel for +11111111111. Error: DB phoneNumber update failure',
status: 'ERROR',
})
})
})
})
......@@ -220,15 +191,15 @@ describe('phone number services -- recycle module', () => {
})
it('notifies the correct instance maintainers', async () => {
await phoneNumberService.recycle({ phoneNumbers })
await recycle(phoneNumbers[0])
expect(broadcastMessageStub.getCall(2).args[0]).to.eql(['+16154804259', '+12345678910'])
expect(broadcastMessageStub.getCall(1).args[0]).to.eql(['+16154804259', '+12345678910'])
})
it('notifies the instance maintainers with a channel failure message', async () => {
await phoneNumberService.recycle({ phoneNumbers })
await recycle(phoneNumbers[0])
expect(broadcastMessageStub.getCall(2).args[1]).to.eql({
expect(broadcastMessageStub.getCall(1).args[1]).to.eql({
messageBody: 'Failed to recycle channel for phone number: +11111111111',
type: 'send',
username: '+15555555555',
......@@ -236,20 +207,12 @@ describe('phone number services -- recycle module', () => {
})
it('returns a failed status', async () => {
const response = await phoneNumberService.recycle({ phoneNumbers })
expect(response).to.eql([
{
message:
'Failed to recycle channel for +11111111111. Error: Failed to destroy channel',
status: 'ERROR',
},
{
message:
'Failed to recycle channel for +12222222222. Error: Failed to destroy channel',
status: 'ERROR',
},
])
const response = await recycle(phoneNumbers[0])
expect(response).to.eql({
message: 'Failed to recycle channel for +11111111111. Error: Failed to destroy channel',
status: 'ERROR',
})
})
})
})
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment