Use ShareLaTeX conventions

This commit is contained in:
James Allen 2014-08-15 12:25:54 +01:00
parent 421647ff63
commit 5c002bc9b1
14 changed files with 502 additions and 117 deletions

View file

@ -0,0 +1,52 @@
module.exports = (grunt) ->
grunt.initConfig
coffee:
app_src:
expand: true,
cwd: "app/coffee"
src: ['**/*.coffee'],
dest: 'app/js/',
ext: '.js'
app:
src: "app.coffee"
dest: "app.js"
unit_tests:
expand: true
cwd: "test/unit/coffee"
src: ["**/*.coffee"]
dest: "test/unit/js/"
ext: ".js"
clean:
app: ["app/js/"]
unit_tests: ["test/unit/js"]
execute:
app:
src: "app.js"
mochaTest:
unit:
options:
reporter: grunt.option('reporter') or 'spec'
src: ["test/unit/js/**/*.js"]
grunt.loadNpmTasks 'grunt-contrib-coffee'
grunt.loadNpmTasks 'grunt-contrib-clean'
grunt.loadNpmTasks 'grunt-mocha-test'
grunt.loadNpmTasks 'grunt-execute'
grunt.loadNpmTasks 'grunt-bunyan'
grunt.registerTask 'compile:app', ['clean:app', 'coffee:app', 'coffee:app_src']
grunt.registerTask 'run', ['compile:app', 'bunyan', 'execute']
grunt.registerTask 'compile:unit_tests', ['clean:unit_tests', 'coffee:unit_tests']
grunt.registerTask 'test:unit', ['compile:app', 'compile:unit_tests', 'mochaTest:unit']
grunt.registerTask 'install', 'compile:app'
grunt.registerTask 'default', ['run']

View file

@ -20,7 +20,8 @@ server.post "/user/:user_id/learn", SpellingAPIController.learn
server.get "/status", (req, res)->
res.send(status:'spelling api is up')
host = Settings.host || "localhost"
port = Settings.port || 3005
server.listen port, host, () ->
console.log "#{server.name} listening at #{host}:#{port}"
host = Settings.internal?.spelling?.host || "localhost"
port = Settings.internal?.spelling?.port || 3005
server.listen port, host, (error) ->
throw error if error?
logger.log "spelling-sharelatex listening at #{host}:#{port}"

View file

@ -47,7 +47,7 @@ class ASpellRunner
word = words[i]
@sendWord(word)
i++
process.nextTick tick
setTimeout tick, 0
else
@close()

View file

@ -1,35 +0,0 @@
redis = require('redis')
settings = require('settings-sharelatex')
rclient = redis.createClient(settings.redis.port, settings.redis.host)
rclient.auth(settings.redis.password)
logger = require('logger-sharelatex')
thirtyMinutes = (60 * 60 * 30)
module.exports =
break: (key, callback)->
rclient.del buildKey(key), callback
set :(key, value, callback)->
value = JSON.stringify value
builtKey = buildKey(key)
multi = rclient.multi()
multi.set builtKey, value
multi.expire builtKey, thirtyMinutes
multi.exec callback
get :(key, callback)->
builtKey = buildKey(key)
rclient.get builtKey, (err, result)->
return callback(err) if err?
if !result?
logger.log key:key, "cache miss"
callback()
else
result = JSON.parse result
logger.log key:key, foundId:result._id, "cache hit"
callback null, result
buildKey = (key)->
return "user-learned-words:#{key}"

View file

@ -0,0 +1,14 @@
module.exports = Settings =
internal:
spelling:
port: 3005
host: "localhost"
redis:
port:6379
host:"127.0.0.1"
password:""
mongo:
url : 'mongodb://127.0.0.1/sharelatex'

View file

@ -1,10 +0,0 @@
module.exports = Settings =
port: 3005
host: "localhost"
redis:
port:6379
host:"127.0.0.1"
password:""
mongo:
url : 'mongodb://127.0.0.1/sharelatexTesting'

View file

@ -7,8 +7,8 @@
"express": "3.1.0",
"async": "0.1.22",
"restify": "2.5.1",
"settings": "git+ssh://git@bitbucket.org:sharelatex/settings-sharelatex.git#master",
"logger": "git+ssh://git@bitbucket.org:sharelatex/logger-sharelatex.git#bunyan",
"settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#master",
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master",
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#master",
"node-statsd": "0.0.3",
"underscore": "1.4.4",
@ -16,8 +16,15 @@
"redis": "~0.8.4"
},
"devDependencies": {
"sinon": "",
"bunyan": "^1.0.0",
"chai": "",
"sandboxed-module": ""
"grunt": "^0.4.5",
"grunt-bunyan": "^0.5.0",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-coffee": "^0.11.0",
"grunt-execute": "^0.2.2",
"grunt-mocha-test": "^0.11.0",
"sandboxed-module": "",
"sinon": ""
}
}

View file

@ -1,56 +0,0 @@
modulePath = "../../../app/js/Cache.js"
should = require('chai').should()
SandboxedModule = require('sandboxed-module')
assert = require('chai').assert
path = require 'path'
user_token = "23ionisou90iilkn"
spellings = ["bob", "smith", "words"]
describe 'Cache', ->
it 'should save the user into redis', (done)->
@redis =
expire: (key, value)->
key.should.equal "user-learned-words:#{user_token}"
(value > 200).should.equal true
set: (key, value)->
key.should.equal "user-learned-words:#{user_token}"
value.should.equal JSON.stringify(spellings)
exec:->
done()
@cache = SandboxedModule.require modulePath, requires:
'redis': createClient :=> {multi:=> @redis}
@cache.set user_token, spellings, ->
it 'should get the user from redis', (done)->
@redis = get: (key, cb)->
key.should.equal "user-learned-words:#{user_token}"
cb(null, JSON.stringify(spellings))
@cache = SandboxedModule.require modulePath, requires:
'redis': createClient :=> return @redis
@cache.get user_token, (err, returnedSpellings)->
assert.deepEqual returnedSpellings, spellings
assert.equal err, null
done()
it 'should return nothing if the key doesnt exist', (done)->
@redis = get: (key, cb)->
cb(null, null)
@cache = SandboxedModule.require modulePath, requires:
'redis': createClient :=> return @redis
@cache.get user_token, (err, founduser)->
assert.equal founduser, undefined
done()
it 'should be able to delete from redis to break cache', (done)->
@redis = del: (key, cb)->
key.should.equal "user-learned-words:#{user_token}"
cb(null)
@cache = SandboxedModule.require modulePath, requires:
'redis': createClient :=> return @redis
@cache.break user_token, done

View file

@ -11,14 +11,8 @@ describe "LearnedWordsManager", ->
@db =
spellingPreferences:
update: sinon.stub().callsArg(3)
@cache =
get: sinon.stub()
set: sinon.stub()
break: sinon.stub()
@LearnedWordsManager = SandboxedModule.require modulePath, requires:
"./DB" : @db
"./Cache":@cache
describe "learnWord", ->
beforeEach ->
@ -41,7 +35,6 @@ describe "LearnedWordsManager", ->
describe "getLearnedWords", ->
beforeEach ->
@cache.get.callsArgWith(1)
@wordList = ["apples", "bananas", "pears"]
@db.spellingPreferences.findOne = (conditions, callback) =>
callback null, learnedWords: @wordList

View file

@ -0,0 +1,112 @@
(function() {
var chai, should, sinon;
sinon = require('sinon');
chai = require('chai');
should = chai.should();
describe("ASpell", function() {
beforeEach(function() {
return this.ASpell = require("../../../app/js/ASpell");
});
describe("a correctly spelled word", function() {
beforeEach(function(done) {
return this.ASpell.checkWords("en", ["word"], (function(_this) {
return function(error, result) {
_this.result = result;
return done();
};
})(this));
});
return it("should not correct the word", function() {
return this.result.length.should.equal(0);
});
});
describe("a misspelled word", function() {
beforeEach(function(done) {
return this.ASpell.checkWords("en", ["bussines"], (function(_this) {
return function(error, result) {
_this.result = result;
return done();
};
})(this));
});
return it("should correct the word", function() {
this.result.length.should.equal(1);
return this.result[0].suggestions.indexOf("business").should.not.equal(-1);
});
});
describe("multiple words", function() {
beforeEach(function(done) {
return this.ASpell.checkWords("en", ["bussines", "word", "neccesary"], (function(_this) {
return function(error, result) {
_this.result = result;
return done();
};
})(this));
});
return it("should correct the incorrect words", function() {
this.result[0].index.should.equal(0);
this.result[0].suggestions.indexOf("business").should.not.equal(-1);
this.result[1].index.should.equal(2);
return this.result[1].suggestions.indexOf("necessary").should.not.equal(-1);
});
});
describe("without a valid language", function() {
beforeEach(function(done) {
return this.ASpell.checkWords("notALang", ["banana"], (function(_this) {
return function(error, result) {
_this.error = error;
_this.result = result;
return done();
};
})(this));
});
return it("should return an error", function() {
return should.exist(this.error);
});
});
describe("when there are no suggestions", function() {
beforeEach(function(done) {
return this.ASpell.checkWords("en", ["asdkfjalkdjfadhfkajsdhfashdfjhadflkjadhflajsd"], (function(_this) {
return function(error, result) {
_this.error = error;
_this.result = result;
return done();
};
})(this));
});
return it("should return a blank array", function() {
this.result.length.should.equal(1);
return this.result[0].suggestions.should.deep.equal([]);
});
});
return describe("when the request times out", function() {
beforeEach(function(done) {
var i, words;
words = (function() {
var _i, _results;
_results = [];
for (i = _i = 0; _i <= 1000000; i = ++_i) {
_results.push("abcdefg");
}
return _results;
})();
this.ASpell.ASPELL_TIMEOUT = 100;
this.start = new Date();
return this.ASpell.checkWords("en", words, (function(_this) {
return function(error, result) {
_this.result = result;
return done();
};
})(this));
});
return it("should return in reasonable time", function(done) {
return done();
});
});
});
}).call(this);

View file

@ -0,0 +1,99 @@
(function() {
var SandboxedModule, chai, expect, modulePath, sinon;
sinon = require('sinon');
chai = require('chai');
expect = chai.expect;
SandboxedModule = require('sandboxed-module');
modulePath = require('path').join(__dirname, '../../../app/js/LearnedWordsManager');
describe("LearnedWordsManager", function() {
beforeEach(function() {
this.token = "a6b3cd919ge";
this.callback = sinon.stub();
this.db = {
spellingPreferences: {
update: sinon.stub().callsArg(3)
}
};
return this.LearnedWordsManager = SandboxedModule.require(modulePath, {
requires: {
"./DB": this.db
}
});
});
describe("learnWord", function() {
beforeEach(function() {
this.word = "instanton";
return this.LearnedWordsManager.learnWord(this.token, this.word, this.callback);
});
it("should insert the word in the word list in the database", function() {
return expect(this.db.spellingPreferences.update.calledWith({
token: this.token
}, {
$push: {
learnedWords: this.word
}
}, {
upsert: true
})).to.equal(true);
});
return it("should call the callback", function() {
return expect(this.callback.called).to.equal(true);
});
});
return describe("getLearnedWords", function() {
beforeEach(function() {
this.wordList = ["apples", "bananas", "pears"];
this.db.spellingPreferences.findOne = (function(_this) {
return function(conditions, callback) {
return callback(null, {
learnedWords: _this.wordList
});
};
})(this);
sinon.spy(this.db.spellingPreferences, "findOne");
return this.LearnedWordsManager.getLearnedWords(this.token, this.callback);
});
it("should get the word list for the given user", function() {
return expect(this.db.spellingPreferences.findOne.calledWith({
token: this.token
})).to.equal(true);
});
return it("should return the word list in the callback", function() {
return expect(this.callback.calledWith(null, this.wordList)).to.equal(true);
});
});
/*
describe "caching the result", ->
it 'should use the cache first if it is primed', (done)->
@wordList = ["apples", "bananas", "pears"]
@cache.get.callsArgWith(1, null, learnedWords: @wordList)
@db.spellingPreferences.findOne = sinon.stub()
@LearnedWordsManager.getLearnedWords @token, (err, spellings)=>
@db.spellingPreferences.find.called.should.equal false
@wordList.should.equal spellings
done()
it 'should set the cache after hitting the db', (done)->
@wordList = ["apples", "bananas", "pears"]
@cache.get.callsArgWith(1)
@db.spellingPreferences.findOne = sinon.stub().callsArgWith(1, null, learnedWords: @wordList)
@LearnedWordsManager.getLearnedWords @token, (err, spellings)=>
@cache.set.calledWith(@token, learnedWords:@wordList).should.equal true
done()
it 'should break cache when update is called', (done)->
@word = "instanton"
@LearnedWordsManager.learnWord @token, @word, =>
@cache.break.calledWith(@token).should.equal true
done()
*/
});
}).call(this);

View file

@ -0,0 +1,208 @@
(function() {
var SandboxedModule, chai, expect, modulePath, sinon;
sinon = require('sinon');
chai = require('chai');
expect = chai.expect;
chai.should();
SandboxedModule = require('sandboxed-module');
modulePath = require('path').join(__dirname, '../../../app/js/SpellingAPIManager');
describe("SpellingAPIManager", function() {
beforeEach(function() {
this.token = "user-id-123";
this.ASpell = {};
this.learnedWords = ["lerned"];
this.LearnedWordsManager = {
getLearnedWords: sinon.stub().callsArgWith(1, null, this.learnedWords),
learnWord: sinon.stub().callsArg(2)
};
return this.SpellingAPIManager = SandboxedModule.require(modulePath, {
requires: {
"./ASpell": this.ASpell,
"./LearnedWordsManager": this.LearnedWordsManager
}
});
});
describe("runRequest", function() {
beforeEach(function() {
this.nonLearnedWords = ["some", "words", "htat", "are", "speled", "rong", "lerned"];
this.allWords = this.nonLearnedWords.concat(this.learnedWords);
this.misspellings = [
{
index: 2,
suggestions: ["that"]
}, {
index: 4,
suggestions: ["spelled"]
}, {
index: 5,
suggestions: ["wrong", "ring"]
}, {
index: 6,
suggestions: ["learned"]
}
];
this.misspellingsWithoutLearnedWords = this.misspellings.slice(0, 3);
this.ASpell.checkWords = (function(_this) {
return function(lang, word, callback) {
return callback(null, _this.misspellings);
};
})(this);
return sinon.spy(this.ASpell, "checkWords");
});
describe("with sensible JSON", function() {
beforeEach(function(done) {
return this.SpellingAPIManager.runRequest(this.token, {
words: this.allWords
}, (function(_this) {
return function(error, result) {
_this.result = result;
return done();
};
})(this));
});
return it("should return the words that are spelled incorrectly and not learned", function() {
return expect(this.result.misspellings).to.deep.equal(this.misspellingsWithoutLearnedWords);
});
});
describe("with a missing words array", function() {
beforeEach(function(done) {
return this.SpellingAPIManager.runRequest(this.token, {}, (function(_this) {
return function(error, result) {
_this.error = error;
_this.result = result;
return done();
};
})(this));
});
return it("should return an error", function() {
return expect(this.error).to.deep.equal(new Error("malformed JSON"));
});
});
describe("with a missing token", function() {
beforeEach(function(done) {
return this.SpellingAPIManager.runRequest(null, {
words: this.allWords
}, (function(_this) {
return function(error, result) {
_this.error = error;
_this.result = result;
return done();
};
})(this));
});
return it("should spell check without using any learned words", function() {
return this.LearnedWordsManager.getLearnedWords.called.should.equal(false);
});
});
describe("without a language", function() {
beforeEach(function(done) {
return this.SpellingAPIManager.runRequest(this.token, {
words: this.allWords
}, (function(_this) {
return function(error, result) {
_this.result = result;
return done();
};
})(this));
});
return it("should use en as the default", function() {
return this.ASpell.checkWords.calledWith("en").should.equal(true);
});
});
describe("with a language", function() {
beforeEach(function(done) {
return this.SpellingAPIManager.runRequest(this.token, {
words: this.allWords,
language: this.language = "fr"
}, (function(_this) {
return function(error, result) {
_this.result = result;
return done();
};
})(this));
});
return it("should use the language", function() {
return this.ASpell.checkWords.calledWith(this.language).should.equal(true);
});
});
return describe("with a very large collection of words", function() {
beforeEach(function(done) {
var i;
this.manyWords = (function() {
var _i, _results;
_results = [];
for (i = _i = 1; _i <= 100000; i = ++_i) {
_results.push("word");
}
return _results;
})();
return this.SpellingAPIManager.runRequest(this.token, {
words: this.manyWords
}, (function(_this) {
return function(error, result) {
_this.result = result;
return done();
};
})(this));
});
return it("should truncate to 10,000 words", function() {
return this.ASpell.checkWords.calledWith(sinon.match.any, this.manyWords.slice(0, 10000)).should.equal(true);
});
});
});
return describe("learnWord", function() {
describe("without a token", function() {
beforeEach(function(done) {
return this.SpellingAPIManager.learnWord(null, {
word: "banana"
}, (function(_this) {
return function(error) {
_this.error = error;
return done();
};
})(this));
});
return it("should return an error", function() {
return expect(this.error).to.deep.equal(new Error("malformed JSON"));
});
});
describe("without a word", function() {
beforeEach(function(done) {
return this.SpellingAPIManager.learnWord(this.token, {}, (function(_this) {
return function(error) {
_this.error = error;
return done();
};
})(this));
});
return it("should return an error", function() {
return expect(this.error).to.deep.equal(new Error("no token provided"));
});
});
return describe("with a word and a token", function() {
beforeEach(function(done) {
this.word = "banana";
return this.SpellingAPIManager.learnWord(this.token, {
word: this.word
}, (function(_this) {
return function(error) {
_this.error = error;
return done();
};
})(this));
});
return it("should call LearnedWordsManager.learnWord", function() {
return this.LearnedWordsManager.learnWord.calledWith(this.token, this.word).should.equal(true);
});
});
});
});
}).call(this);