Просмотр исходного кода

Add support for removing words from user dictionaries to admin panel (#2371)

* Add support for deletion of words from user dictionary to admin-panel

Co-authored-by: Jessica Lawshe <jessica.lawshe@overleaf.com>

* Add confirmation modal to dictionary word deletion

* Improve dictionary view with some helpful text

* Add MockSpellingApi

* Handle errors more cleanly in SpellingHandler

GitOrigin-RevId: a7d7f8bad120a15b0eaa7d77b5ee804998477ed1
Simon Detheridge 6 лет назад
Родитель
Сommit
81103c93e6

+ 94 - 0
services/web/app/src/Features/Spelling/SpellingHandler.js

@@ -0,0 +1,94 @@
+const request = require('request')
+const Settings = require('settings-sharelatex')
+const OError = require('@overleaf/o-error')
+
+const TIMEOUT = 10 * 1000
+
+module.exports = {
+  getUserDictionary(userId, callback) {
+    const url = `${Settings.apis.spelling.url}/user/${userId}`
+    request.get({ url: url, timeout: TIMEOUT }, (error, response) => {
+      if (error) {
+        return callback(
+          new OError({
+            message: 'error getting user dictionary',
+            info: { error, userId }
+          }).withCause(error)
+        )
+      }
+
+      if (response.statusCode < 200 || response.statusCode >= 300) {
+        return callback(
+          new OError({
+            message:
+              'Non-success code from spelling API when getting user dictionary',
+            info: { userId, statusCode: response.statusCode }
+          })
+        )
+      }
+
+      callback(null, JSON.parse(response.body))
+    })
+  },
+
+  deleteWordFromUserDictionary(userId, word, callback) {
+    const url = `${Settings.apis.spelling.url}/user/${userId}/unlearn`
+    request.post(
+      {
+        url: url,
+        json: {
+          word
+        },
+        timeout: TIMEOUT
+      },
+      (error, response) => {
+        if (error) {
+          return callback(
+            new OError({
+              message: 'error deleting word from user dictionary',
+              info: { error, userId, word }
+            }).withCause(error)
+          )
+        }
+
+        if (response.statusCode < 200 || response.statusCode >= 300) {
+          return callback(
+            new OError({
+              message:
+                'Non-success code from spelling API when removing word from user dictionary',
+              info: { userId, word, statusCode: response.statusCode }
+            })
+          )
+        }
+
+        callback()
+      }
+    )
+  },
+
+  deleteUserDictionary(userId, callback) {
+    const url = `${Settings.apis.spelling.url}/user/${userId}`
+    request.delete({ url: url, timeout: TIMEOUT }, (error, response) => {
+      if (error) {
+        return callback(
+          new OError({
+            message: 'error deleting user dictionary',
+            info: { userId }
+          }).withCause(error)
+        )
+      }
+
+      if (response.statusCode < 200 || response.statusCode >= 300) {
+        return callback(
+          new OError({
+            message:
+              'Non-success code from spelling API when removing user dictionary',
+            info: { userId, statusCode: response.statusCode }
+          })
+        )
+      }
+
+      callback()
+    })
+  }
+}

+ 59 - 0
services/web/test/acceptance/src/helpers/MockSpellingApi.js

@@ -0,0 +1,59 @@
+const express = require('express')
+const app = express()
+
+const MockSpellingApi = {
+  words: {},
+
+  run() {
+    app.get('/user/:userId', (req, res) => {
+      const { userId } = req.params
+      const words = this.words[userId] || []
+      res.json(words)
+    })
+
+    app.delete('/user/:userId', (req, res) => {
+      const { userId } = req.params
+      this.words.delete(userId)
+      res.sendStatus(200)
+    })
+
+    app.post('/user/:userId/learn', (req, res) => {
+      const word = req.body.word
+      const { userId } = req.params
+      if (word) {
+        this.words[userId] = this.words[userId] || []
+        if (!this.words[userId].includes(word)) {
+          this.words[userId].push(word)
+        }
+      }
+      res.sendStatus(200)
+    })
+
+    app.post('/user/:userId/unlearn', (req, res) => {
+      const word = req.body.word
+      const { userId } = req.params
+      if (word && this.words[userId]) {
+        const wordIndex = this.words[userId].indexOf(word)
+        if (wordIndex !== -1) {
+          this.words[userId].splice(wordIndex, 1)
+        }
+      }
+      res.sendStatus(200)
+    })
+
+    app
+      .listen(3005, error => {
+        if (error) {
+          throw error
+        }
+      })
+      .on('error', error => {
+        console.error('error starting MockSpellingApi:', error.message)
+        process.exit(1)
+      })
+  }
+}
+
+MockSpellingApi.run()
+
+module.exports = MockSpellingApi

+ 129 - 0
services/web/test/unit/src/Spelling/SpellingHandlerTests.js

@@ -0,0 +1,129 @@
+const SandboxedModule = require('sandboxed-module')
+const { expect } = require('chai')
+const sinon = require('sinon')
+const modulePath = require('path').join(
+  __dirname,
+  '../../../../app/src/Features/Spelling/SpellingHandler.js'
+)
+
+const TIMEOUT = 1000 * 10
+
+const SPELLING_HOST = 'http://spelling.service.test'
+const SPELLING_URL = 'http://spelling.service.test'
+
+describe('SpellingHandler', function() {
+  let userId, word, dictionary, dictionaryString, request, SpellingHandler
+
+  beforeEach(function() {
+    userId = 'wombat'
+    word = 'potato'
+    dictionary = ['wombaat', 'woombat']
+    dictionaryString = JSON.stringify(dictionary)
+    request = {
+      get: sinon
+        .stub()
+        .yields(null, { statusCode: 200, body: dictionaryString }),
+      post: sinon.stub().yields(null, { statusCode: 204 }),
+      delete: sinon.stub().yields(null, { statusCode: 204 })
+    }
+
+    SpellingHandler = SandboxedModule.require(modulePath, {
+      requires: {
+        request: request,
+        'logger-sharelatex': {
+          warn() {},
+          error() {},
+          info() {}
+        },
+        'settings-sharelatex': {
+          apis: { spelling: { host: SPELLING_HOST, url: SPELLING_URL } }
+        }
+      }
+    })
+  })
+
+  describe('getUserDictionary', function() {
+    it('calls the spelling API', function(done) {
+      SpellingHandler.getUserDictionary(userId, () => {
+        expect(request.get).to.have.been.calledWith({
+          url: 'http://spelling.service.test/user/wombat',
+          timeout: TIMEOUT
+        })
+        done()
+      })
+    })
+
+    it('returns the dictionary', function(done) {
+      SpellingHandler.getUserDictionary(userId, (err, dictionary) => {
+        expect(err).not.to.exist
+        expect(dictionary).to.deep.equal(dictionary)
+        done()
+      })
+    })
+
+    it('returns an error when the request fails', function(done) {
+      request.get = sinon.stub().yields(new Error('ugh'))
+      SpellingHandler.getUserDictionary(userId, err => {
+        expect(err).to.exist
+        done()
+      })
+    })
+  })
+
+  describe('deleteWordFromUserDictionary', function() {
+    it('calls the spelling API', function(done) {
+      SpellingHandler.deleteWordFromUserDictionary(userId, word, () => {
+        expect(request.post).to.have.been.calledWith({
+          url: 'http://spelling.service.test/user/wombat/unlearn',
+          json: {
+            word: word
+          },
+          timeout: TIMEOUT
+        })
+        done()
+      })
+    })
+
+    it('does not return an error', function(done) {
+      SpellingHandler.deleteWordFromUserDictionary(userId, word, err => {
+        expect(err).not.to.exist
+        done()
+      })
+    })
+
+    it('returns an error when the request fails', function(done) {
+      request.post = sinon.stub().yields(new Error('ugh'))
+      SpellingHandler.deleteWordFromUserDictionary(userId, word, err => {
+        expect(err).to.exist
+        done()
+      })
+    })
+  })
+
+  describe('deleteUserDictionary', function() {
+    it('calls the spelling API', function(done) {
+      SpellingHandler.deleteUserDictionary(userId, () => {
+        expect(request.delete).to.have.been.calledWith({
+          url: 'http://spelling.service.test/user/wombat',
+          timeout: TIMEOUT
+        })
+        done()
+      })
+    })
+
+    it('does not return an error', function(done) {
+      SpellingHandler.deleteUserDictionary(userId, err => {
+        expect(err).not.to.exist
+        done()
+      })
+    })
+
+    it('returns an error when the request fails', function(done) {
+      request.delete = sinon.stub().yields(new Error('ugh'))
+      SpellingHandler.deleteUserDictionary(userId, err => {
+        expect(err).to.exist
+        done()
+      })
+    })
+  })
+})