diff --git a/services/spelling/app.js b/services/spelling/app.js index 30024dc230..cf2065f902 100644 --- a/services/spelling/app.js +++ b/services/spelling/app.js @@ -30,6 +30,7 @@ server.del('/user/:user_id', SpellingAPIController.deleteDic) server.get('/user/:user_id', SpellingAPIController.getDic) server.post('/user/:user_id/check', SpellingAPIController.check) server.post('/user/:user_id/learn', SpellingAPIController.learn) +server.post('/user/:user_id/unlearn', SpellingAPIController.unlearn) server.get('/status', (req, res) => res.send({ status: 'spelling api is up' })) server.get('/health_check', HealthCheckController.healthCheck) diff --git a/services/spelling/app/js/LearnedWordsManager.js b/services/spelling/app/js/LearnedWordsManager.js index adeb9b7dd4..bbf7651d87 100644 --- a/services/spelling/app/js/LearnedWordsManager.js +++ b/services/spelling/app/js/LearnedWordsManager.js @@ -24,6 +24,22 @@ const LearnedWordsManager = { ) }, + unlearnWord(userToken, word, callback) { + if (callback == null) { + callback = () => {} + } + mongoCache.del(userToken) + return db.spellingPreferences.update( + { + token: userToken + }, + { + $pull: { learnedWords: word } + }, + callback + ) + }, + getLearnedWords(userToken, callback) { if (callback == null) { callback = () => {} @@ -61,6 +77,7 @@ const LearnedWordsManager = { const promises = { learnWord: promisify(LearnedWordsManager.learnWord), + unlearnWord: promisify(LearnedWordsManager.unlearnWord), getLearnedWords: promisify(LearnedWordsManager.getLearnedWords), deleteUsersLearnedWords: promisify( LearnedWordsManager.deleteUsersLearnedWords @@ -70,7 +87,7 @@ const promises = { LearnedWordsManager.promises = promises module.exports = LearnedWordsManager -;['learnWord', 'getLearnedWords'].map(method => +;['learnWord', 'unlearnWord', 'getLearnedWords'].map(method => metrics.timeAsyncMethod( LearnedWordsManager, method, diff --git a/services/spelling/app/js/SpellingAPIController.js b/services/spelling/app/js/SpellingAPIController.js index 2239d997e0..e23b09da5d 100644 --- a/services/spelling/app/js/SpellingAPIController.js +++ b/services/spelling/app/js/SpellingAPIController.js @@ -49,6 +49,19 @@ module.exports = { }) }, + unlearn(req, res, next) { + metrics.inc('spelling-unlearn', 0.1) + const { token, word } = extractLearnRequestData(req) + logger.info({ token, word }, 'unlearning word') + SpellingAPIManager.unlearnWord(token, req.body, function(error) { + if (error != null) { + return next(error) + } + res.sendStatus(200) + next() + }) + }, + deleteDic(req, res, next) { const { token, word } = extractLearnRequestData(req) logger.log({ token, word }, 'deleting user dictionary') diff --git a/services/spelling/app/js/SpellingAPIManager.js b/services/spelling/app/js/SpellingAPIManager.js index 7c330eb312..3c437d929f 100644 --- a/services/spelling/app/js/SpellingAPIManager.js +++ b/services/spelling/app/js/SpellingAPIManager.js @@ -30,6 +30,20 @@ const SpellingAPIManager = { return LearnedWordsManager.learnWord(token, request.word, callback) }, + unlearnWord(token, request, callback) { + if (callback == null) { + callback = () => {} + } + if (request.word == null) { + return callback(new Error('malformed JSON')) + } + if (token == null) { + return callback(new Error('no token provided')) + } + + return LearnedWordsManager.unlearnWord(token, request.word, callback) + }, + deleteDic(token, callback) { return LearnedWordsManager.deleteUsersLearnedWords(token, callback) }, diff --git a/services/spelling/test/acceptance/js/LearnTest.js b/services/spelling/test/acceptance/js/LearnTest.js index 6f6bc21a2e..8e363b4e10 100644 --- a/services/spelling/test/acceptance/js/LearnTest.js +++ b/services/spelling/test/acceptance/js/LearnTest.js @@ -19,6 +19,14 @@ const learnWord = word => }) }) +const unlearnWord = word => + request.post({ + url: `/user/${USER_ID}/unlearn`, + body: JSON.stringify({ + word + }) + }) + const deleteDict = () => request.del({ url: `/user/${USER_ID}` @@ -51,3 +59,24 @@ describe('learning words', () => { expect(responseBody.misspellings.length).to.equals(1) }) }) + +describe('unlearning words', () => { + it('should return status 200 when posting a word successfully', async () => { + const response = await unlearnWord('anything') + expect(response.statusCode).to.equal(200) + }) + + it('should return misspellings after a word is unlearnt', async () => { + await learnWord('abv') + + const response = await checkWord(['abv']) + const responseBody = JSON.parse(response.body) + expect(responseBody.misspellings.length).to.equals(0) + + await unlearnWord('abv') + + const response2 = await checkWord(['abv']) + const responseBody2 = JSON.parse(response2.body) + expect(responseBody2.misspellings.length).to.equals(1) + }) +}) diff --git a/services/spelling/test/unit/js/LearnedWordsManagerTests.js b/services/spelling/test/unit/js/LearnedWordsManagerTests.js index 76122b1d5c..3d2324c22c 100644 --- a/services/spelling/test/unit/js/LearnedWordsManagerTests.js +++ b/services/spelling/test/unit/js/LearnedWordsManagerTests.js @@ -24,7 +24,7 @@ describe('LearnedWordsManager', function() { this.callback = sinon.stub() this.db = { spellingPreferences: { - update: sinon.stub().callsArg(3) + update: sinon.stub().yields() } } this.cache = { @@ -80,6 +80,34 @@ describe('LearnedWordsManager', function() { }) }) + describe('unlearnWord', function() { + beforeEach(function() { + this.word = 'instanton' + return this.LearnedWordsManager.unlearnWord( + this.token, + this.word, + this.callback + ) + }) + + it('should remove the word from the word list in the database', function() { + return expect( + this.db.spellingPreferences.update.calledWith( + { + token: this.token + }, + { + $pull: { learnedWords: this.word } + } + ) + ).to.equal(true) + }) + + return it('should call the callback', function() { + return expect(this.callback.called).to.equal(true) + }) + }) + describe('getLearnedWords', function() { beforeEach(function() { this.wordList = ['apples', 'bananas', 'pears'] diff --git a/services/spelling/test/unit/js/SpellingAPIManagerTests.js b/services/spelling/test/unit/js/SpellingAPIManagerTests.js index 5baecbc8b0..f2e1bc55cc 100644 --- a/services/spelling/test/unit/js/SpellingAPIManagerTests.js +++ b/services/spelling/test/unit/js/SpellingAPIManagerTests.js @@ -21,6 +21,7 @@ describe('SpellingAPIManager', function() { this.LearnedWordsManager = { getLearnedWords: sinon.stub().callsArgWith(1, null, this.learnedWords), learnWord: sinon.stub().callsArg(2), + unlearnWord: sinon.stub().callsArg(2), promises: { getLearnedWords: sinon.stub().returns(promiseStub(this.learnedWords)) } @@ -229,4 +230,56 @@ describe('SpellingAPIManager', function() { }) }) }) + + describe('unlearnWord', function() { + describe('without a token', function() { + beforeEach(function(done) { + this.SpellingAPIManager.unlearnWord(null, { word: 'banana' }, error => { + this.error = error + done() + }) + }) + + it('should return an error', function() { + expect(this.error).to.exist + expect(this.error).to.be.instanceof(Error) + expect(this.error.message).to.equal('no token provided') + }) + }) + + describe('without a word', function() { + beforeEach(function(done) { + this.SpellingAPIManager.unlearnWord(this.token, {}, error => { + this.error = error + done() + }) + }) + + it('should return an error', function() { + expect(this.error).to.exist + expect(this.error).to.be.instanceof(Error) + expect(this.error.message).to.equal('malformed JSON') + }) + }) + + describe('with a word and a token', function() { + beforeEach(function(done) { + this.word = 'banana' + this.SpellingAPIManager.unlearnWord( + this.token, + { word: this.word }, + error => { + this.error = error + done() + } + ) + }) + + it('should call LearnedWordsManager.unlearnWord', function() { + this.LearnedWordsManager.unlearnWord + .calledWith(this.token, this.word) + .should.equal(true) + }) + }) + }) })