diff --git a/lib/models/contact.js b/lib/models/contact.js index 6d89dbe4..e2491dba 100644 --- a/lib/models/contact.js +++ b/lib/models/contact.js @@ -142,13 +142,23 @@ ContactSchema.methods.recordPoints = function(points) { return this; }; +/** + * Will reset the timeout rate after 72 hours without a timeout. + */ +ContactSchema.methods.resetTimeoutRate = function() { + const window = 259200000; // 72 hours + if (this.timeoutRate && this.lastTimeout < Date.now() - window) { + this.timeoutRate = 0; + } +}; + /** * Will update the lastTimeout and calculate the timeoutRate based - * on a 24 hour window of activity. + * on a 72 hour window of activity. */ ContactSchema.methods.recordTimeoutFailure = function() { const now = Date.now(); - const window = 86400000; // 24 hours + const window = 259200000; // 72 hours const lastTimeoutRate = this.timeoutRate || 0; if (this.lastTimeout && this.lastTimeout > this.lastSeen) { @@ -191,6 +201,9 @@ ContactSchema.methods.recordResponseTime = function(responseTime) { this.responseTime = newResponseTime; + // Make sure to reset timeout rate if possible + this.resetTimeoutRate(); + return this; }; diff --git a/lib/models/cron.js b/lib/models/cron.js new file mode 100644 index 00000000..8464135f --- /dev/null +++ b/lib/models/cron.js @@ -0,0 +1,140 @@ +'use strict'; + +const assert = require('assert'); +const mongoose = require('mongoose'); + +const CronJob = new mongoose.Schema({ + name: { + type: mongoose.Schema.Types.String, + required: true, + unique: true, + index: true + }, + locked: { + type: mongoose.Schema.Types.Boolean, + required: true + }, + lockedEnd: { + type: mongoose.Schema.Types.Date, + required: true + }, + started: { + type: mongoose.Schema.Types.Date, + required: true + }, + finished: { + type: mongoose.Schema.Types.Date, + required: false + }, + rawData: { + type: mongoose.Schema.Types.Mixed, + required: false + } +}); + +CronJob.statics.unlock = function(name, rawData, callback) { + assert(typeof name === 'string', 'data is expected to be a string'); + const now = new Date(); + const query = { + name: name + }; + const sort = {}; + const update = { + $set: { + locked: false, + finished: now, + rawData: rawData + } + }; + const options = { + new: true, + writeConcern: 'majority' + }; + this.collection.findAndModify(query, sort, update, options, callback); +}; + +CronJob.statics.lock = function(name, expires, callback) { + const self = this; + const now = new Date(); + const end = new Date(now.getTime() + expires); + assert(Number.isInteger(expires), 'Expires argument is expected'); + + function isDuplicate(err) { + return (err && err.code === 11000); + } + + function getLock(done) { + const query = { + name: name, + locked: false + }; + const sort = {}; + const update = { + $set: { + name: name, + locked: true, + lockedEnd: end, + started: now + } + }; + const options = { + new: true, + upsert: true, + writeConcern: 'majority' + }; + self.collection.findAndModify(query, sort, update, options, (err, res) => { + if (isDuplicate(err)) { + return done(null, false); + } else if (err) { + return done(err, false); + } + done(null, true, res); + }); + } + + function getLockFromExpired(done) { + const query = { + name: name, + locked: true, + lockedEnd: { + $lte: now + } + }; + const sort = {}; + const update = { + $set: { + name: name, + locked: true, + lockedEnd: end, + started: now + } + }; + const options = { + new: true, + upsert: true, + writeConcern: 'majority' + }; + self.collection.findAndModify(query, sort, update, options, (err, res) => { + if (isDuplicate(err)) { + return done(null, false); + } else if (err) { + return done(err, false); + } + done(null, true, res); + }); + } + + getLock((err, success, res) => { + if (err) { + return callback(err); + } + if (!success) { + return getLockFromExpired(callback); + } + callback(err, success, res); + }); +}; + +module.exports = function(connection) { + return connection.model('CronJob', CronJob); +}; diff --git a/lib/models/index.js b/lib/models/index.js index c2b39e88..4d8f8090 100644 --- a/lib/models/index.js +++ b/lib/models/index.js @@ -3,6 +3,7 @@ module.exports = { Bucket: require('./bucket'), PublicKey: require('./pubkey'), + CronJob: require('./cron'), User: require('./user'), UserNonce: require('./usernonce'), Token: require('./token'), diff --git a/lib/models/storage-event.js b/lib/models/storage-event.js index 325252ad..f26b40fa 100644 --- a/lib/models/storage-event.js +++ b/lib/models/storage-event.js @@ -3,43 +3,104 @@ const mongoose = require('mongoose'); const utils = require('../utils'); /** - * Represents the history of storage related summary + * Represents the history of storage related summary * statistics for buckets and their associated files * @constructor */ var StorageEvent = new mongoose.Schema({ - bucket: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Bucket', - required: true - }, - bucketEntry: { - type: mongoose.Schema.Types.ObjectId, - ref: 'BucketEntry', - required: true + token: { + type: String, + required: false, + unique: true }, user: { - type: String, + type: String, ref: 'User', - required: true, + required: false, validate: { - validator: value => utils.isValidEmail(value), + validator: value => utils.isValidEmail(value) || !value, message: 'Invalid user email address' } }, + client: { + type: String, + required: true + }, + farmer: { + type: String, + required: true + }, timestamp: { type: Date, default: Date.now, required: true }, + farmerEnd: { + type: Date, + required: false + }, + userDeleted: { + type: Date, + required: false + }, downloadBandwidth: { type: Number, required: false }, storage: { - type: Number, + type: Number, required: false + }, + shardHash: { + type: String, + required: false + }, + clientReport: { + exchangeStart: { + type: Date, + required: false + }, + exchangeEnd: { + type: Date, + required: false + }, + exchangeResultCode: { + type: Number, + required: false + }, + exchangeResultMessage: { + type: String, + required: false + } + }, + farmerReport: { + exchangeStart: { + type: Date, + required: false + }, + exchangeEnd: { + type: Date, + required: false + }, + exchangeResultCode: { + type: Number, + required: false + }, + exchangeResultMessage: { + type: String, + required: false + } + }, + success: { + type: Boolean, + required: true, + default: false + }, + processed: { + type: Boolean, + required: false, + default: false } }); diff --git a/lib/models/user.js b/lib/models/user.js index 88383e43..b96982ea 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -1,5 +1,6 @@ 'use strict'; +const assert = require('assert'); const ms = require('ms'); const errors = require('storj-service-error-types'); const activator = require('hat').rack(256); @@ -74,6 +75,20 @@ var UserSchema = new mongoose.Schema({ required: true } }, + reports: { + totalRate: { + type: Number, + required: false + }, + unknownRate: { + type: Number, + required: false + }, + timestamp: { + type: Date, + required: false + } + }, bytesUploaded: { lastHourStarted: { type: Date, @@ -135,9 +150,7 @@ var UserSchema = new mongoose.Schema({ UserSchema.plugin(SchemaOptions); -UserSchema.index({ - resetter: 1 -}); +UserSchema.index({ resetter: 1 }); UserSchema.set('toJSON', { virtuals: true, @@ -342,6 +355,131 @@ UserSchema.methods.deactivate = function(callback) { this.save(callback); }; +/** + * Checks if the rates of reports resolving to unknown exceed thresholds + * @param {Number} threshold - A number between 0 and 1 + */ +UserSchema.methods.exceedsUnknownReportsThreshold = function(threshold) { + const totalRate = this.reports ? this.reports.totalRate : undefined; + const unknownRate = this.reports ? this.reports.unknownRate : undefined; + if (!unknownRate || !totalRate) { + return false; + } + if (totalRate / unknownRate > threshold) { + return true; + } + return false; +}; + + +/** + * Will update reporting statistics for a user + * + * Rates are stored as an estimated moving average of the time between + * reports. The lower the number the faster and more frequent the reports are + * on average. + * + * @param {Boolean} unknown + * @param {Date} timestamp + * @param {Number} bytes + * @param {Function} callback + */ +UserSchema.methods.updateUnknownReports = function(unknown, + timestamp, + bytes, + callback) { + + assert(typeof unknown === 'boolean', 'unknown is expected to be boolean'); + assert(timestamp instanceof Date, 'timestamp is expected to be a Date'); + + /** + * Calculates estimated moving average + * @param {Number} a - Previous average + * @param {Number} b - Value to update average + */ + function ema(a, b) { + const p = 5000; // The number of reports in decay + const k = 2 / (p + 1); + return b * k + a * (1 - k); + } + + const defaultDelta = 5000; + + let totalPrevRate = 0; + if (this.reports && this.reports.totalRate) { + totalPrevRate = this.reports.totalRate; + } + let totalPrevTimestamp = 0; + if (this.reports && this.reports.timestamp) { + totalPrevTimestamp = this.reports.timestamp; + } + + // Check that this update hasn't already been applied + if (totalPrevTimestamp > timestamp) { + // TODO give error? + return callback(); + } + + // Prevent totalDelta being negative, 0, null, undefined or NaN + if (!totalPrevTimestamp || timestamp <= totalPrevTimestamp) { + totalPrevTimestamp = timestamp - defaultDelta; + } + + // Also keep track of the number of bytes being reported + // to give larger weight to larger reports + const totalDelta = timestamp - totalPrevTimestamp; + const totalBandwidth = bytes / totalDelta; + const totalRate = ema(totalPrevRate, totalBandwidth); + + let update = { + $set: { + 'reports.totalRate': totalRate, + 'reports.timestamp': timestamp + } + }; + + // If the report has an unknown status also update the rate + // for unknown reports + if (unknown) { + let unknownPrevRate = 0; + let unknownPrevTimestamp = 0; + if (this.reports && this.reports.unknownRate) { + unknownPrevRate = this.reports.unknownRate; + } + if (this.reports && this.reports.timestamp) { + unknownPrevTimestamp = this.reports.timestamp; + } + + if (!unknownPrevTimestamp) { + unknownPrevTimestamp = timestamp - defaultDelta; + } + const unknownDelta = timestamp - unknownPrevTimestamp; + const unknownBandwidth = bytes / unknownDelta; + const unknownRate = ema(unknownPrevRate, unknownBandwidth); + + update.$set['reports.unknownRate'] = unknownRate; + update.$set['reports.timestamp'] = timestamp; + } else { + update.$set['reports.timestamp'] = timestamp; + } + + // Make sure to only update if not already updated by using + // the timestamp that is updated every time. + this.collection.update({ + '_id': this._id, + $or: [ + { 'reports.timestamp': { $lt: timestamp } }, + { 'reports.timestamp': { $exists: false } }, + ] + }, update, (err) => { + if (err) { + return callback(err); + } + // TODO handle res? + callback(null); + }); +}; + UserSchema.statics.create = function(opts, email, passwd, callback) { const User = this; const a = arguments; diff --git a/test/contact.unit.js b/test/contact.unit.js index f9d96f4a..ada7407c 100644 --- a/test/contact.unit.js +++ b/test/contact.unit.js @@ -189,6 +189,31 @@ describe('Storage/models/Contact', function() { }); }); + describe('#resetTimeoutRate', function() { + it('will reset the timeoutRate after 72 hour window', function() { + const past = new Date(new Date().getTime() - 259200000 - 1); + const contact = new Contact({ + lastSeen: Date.now(), + lastTimeout: past, + timeoutRate: 1 + }); + expect(contact.timeoutRate).to.equal(1); + contact.resetTimeoutRate(); + expect(contact.timeoutRate).to.equal(0); + }); + it('will not reset the timeoutRate before 72 hour window', function() { + const past = new Date(new Date().getTime() - 259200000 + 1); + const contact = new Contact({ + lastSeen: Date.now(), + lastTimeout: past, + timeoutRate: 1 + }); + expect(contact.timeoutRate).to.equal(1); + contact.resetTimeoutRate(); + expect(contact.timeoutRate).to.equal(1); + }); + }); + describe('#recordTimeoutFailure', function() { const sandbox = sinon.sandbox.create(); afterEach(() => sandbox.restore()); @@ -214,7 +239,7 @@ describe('Storage/models/Contact', function() { clock.tick(600000); // 10 min contact.recordTimeoutFailure(); - expect(contact.timeoutRate.toFixed(4)).to.equal('0.0069'); + expect(contact.timeoutRate.toFixed(4)).to.equal('0.0023'); }); it('0.5 after 12 hours of failure', function() { @@ -232,10 +257,10 @@ describe('Storage/models/Contact', function() { contact.recordTimeoutFailure(); } - expect(contact.timeoutRate.toFixed(4)).to.equal('0.5000'); + expect(contact.timeoutRate.toFixed(4)).to.equal('0.1667'); }); - it('1 after 24 hours of failure', function() { + it('1 after 72 hours of failure', function() { const clock = sandbox.useFakeTimers(); const contact = new Contact({ lastSeen: Date.now() @@ -247,8 +272,8 @@ describe('Storage/models/Contact', function() { clock.tick(600000); // 10 min contact.recordTimeoutFailure(); - // 24 repeated failures, each over an hour - for (var i = 0; i < 24; i++) { + // 72 repeated failures, each over an hour + for (var i = 0; i < 72; i++) { clock.tick(3600000); // 1 hour contact.recordTimeoutFailure(); } @@ -282,10 +307,10 @@ describe('Storage/models/Contact', function() { contact.recordTimeoutFailure(); } - expect(contact.timeoutRate.toFixed(2)).to.equal('0.45'); + expect(contact.timeoutRate.toFixed(2)).to.equal('0.15'); }); - it('will reset after 24 hours', function() { + it('will reset after 72 hours', function() { const clock = sandbox.useFakeTimers(); const contact = new Contact({ lastSeen: Date.now() @@ -301,10 +326,10 @@ describe('Storage/models/Contact', function() { contact.recordTimeoutFailure(); } - expect(contact.timeoutRate.toFixed(2)).to.equal('0.25'); + expect(contact.timeoutRate.toFixed(2)).to.equal('0.08'); // 24 hours passed with successful queries - for (let i = 0; i < 24; i++) { + for (let i = 0; i < 72; i++) { clock.tick(3600000); // 1 hour contact.lastSeen = Date.now(); } @@ -315,7 +340,7 @@ describe('Storage/models/Contact', function() { contact.recordTimeoutFailure(); } - expect(contact.timeoutRate.toFixed(2)).to.equal('0.04'); + expect(contact.timeoutRate.toFixed(2)).to.equal('0.01'); }); }); @@ -355,6 +380,14 @@ describe('Storage/models/Contact', function() { expect(contact.responseTime).to.be.above(10000); }); + it('will reset timeout rate', function() { + const contact = new Contact({}); + contact.resetTimeoutRate = sinon.stub(); + contact.recordResponseTime(15000); + expect(contact.responseTime).to.be.above(10000); + expect(contact.resetTimeoutRate.callCount).to.equal(1); + }); + it('will improve response times with a fast response', function() { const contact = new Contact({}); contact.recordResponseTime(100); diff --git a/test/cron.unit.js b/test/cron.unit.js new file mode 100644 index 00000000..1d22f3b7 --- /dev/null +++ b/test/cron.unit.js @@ -0,0 +1,152 @@ +'use strict'; + +const chai = require('chai'); +const expect = chai.expect; +const sinon = require('sinon'); +const chaiDate = require('chai-datetime'); +const mongoose = require('mongoose'); + +chai.use(chaiDate); +require('mongoose-types').loadTypes(mongoose); + +const CronSchema = require('../lib/models/cron'); + +var Cron; +var connection; + +before(function(done) { + connection = mongoose.createConnection( + 'mongodb://127.0.0.1:27017/__storj-bridge-test', + function() { + Cron = CronSchema(connection); + Cron.remove({}, function() { + done(); + }); + } + ); +}); + +after(function(done) { + connection.close(done); +}); + +describe('/Storage/models/Cron', function() { + const sandbox = sinon.sandbox.create(); + afterEach(() => sandbox.restore()); + + describe('@constructor', function() { + + it('should create new work', function (done) { + const now = new Date(); + const end = new Date(now.getTime() + 1000); + const job = new Cron({ + name: 'StorageEventsFinality', + locked: true, + lockedEnd: end, + started: now + }); + + job.save(function(err, job) { + if (err) { + return done(err); + } + + expect(job.name).to.equal('StorageEventsFinality'); + expect(job.locked).to.equal(true); + expect(job.lockedEnd).to.equal(end); + expect(job.started).to.equal(now); + done(); + }); + }); + }); + + describe('#lock', function () { + + it('should get lock without document', function(done) { + + Cron.lock('SingletonOne', 1000, function(err) { + if (err) { + return done(err); + } + done(); + }); + + }); + + it('should not get lock with existing locked document', function(done) { + + Cron.lock('SingletonTwo', 10000, function(err, locked) { + if (err) { + return done(err); + } + expect(locked).to.equal(true); + + Cron.lock('SingletonTwo', 10000, function(err, locked) { + if (err) { + return done(err); + } + expect(locked).to.equal(false); + done(); + }); + }); + + }); + + it('should get lock with expired locked document', function(done) { + const now = new Date(); + const clock = sandbox.useFakeTimers(); + clock.tick(now.getTime()); + const expires = 10000; + + Cron.lock('SingletonThree', expires, function(err, locked, res) { + if (err) { + return done(err); + } + expect(locked).to.equal(true); + expect(res); + clock.tick(expires); + + Cron.lock('SingletonThree', expires, function(err, locked, res) { + if (err) { + return done(err); + } + expect(locked).to.equal(true); + expect(res); + done(); + }); + }); + }); + + }); + + describe('#unlock', function () { + it('should unlock and save raw data', function(done) { + Cron.lock('SingletonFour', 10000, (err) => { + if (err) { + return done(err); + } + + const rawData = { + message: 'Hello from the previous job' + }; + + Cron.unlock('SingletonFour', rawData, (err, res) => { + if (err) { + return done(err); + } + expect(res.value.locked).to.equal(false); + + Cron.lock('SingletonFour', 10000, (err, success, res) => { + if (err) { + return done(err); + } + expect(success).to.equal(true); + expect(res.value.rawData.message).to.equal(rawData.message); + done(); + }); + }); + }); + }); + }); + +}); diff --git a/test/storage-event.unit.js b/test/storage-event.unit.js index cc1f135c..bbefbf5d 100644 --- a/test/storage-event.unit.js +++ b/test/storage-event.unit.js @@ -30,22 +30,48 @@ describe('Storage/models/Storage-Event', function() { it('should create storage event with default props', function(done) { var newStorageEvent = new StorageEvent({ - bucket: mongoose.Types.ObjectId(), - bucketEntry: mongoose.Types.ObjectId(), user: 'user@gmail.com', downloadBandwidth: 0, storage: 1000000, + farmer: 'c5857d99d3ff951701093b75fba94e1c82877f9b', + client: 'user@gmail.com' }); - newStorageEvent.save(function(err, storEvent) { + newStorageEvent.save(function(err, storeEvent) { + if (err) { + return done(err); + } let ObjectIdType = mongoose.Types.ObjectId; expect(err).to.not.be.an.instanceOf(Error); - expect(storEvent.bucket).to.be.an.instanceOf(ObjectIdType); - expect(storEvent.bucketEntry).to.be.an.instanceOf(ObjectIdType); - expect(storEvent.user).to.be.a('string'); - expect(storEvent.timestamp).to.be.an.instanceOf(Date); - expect(storEvent.downloadBandwidth).to.be.a('number'); - expect(storEvent.storage).to.be.a('number'); + expect(storeEvent._id).to.be.instanceOf(ObjectIdType); + expect(storeEvent.user).to.be.a('string'); + expect(storeEvent.timestamp).to.be.an.instanceOf(Date); + expect(storeEvent.downloadBandwidth).to.be.a('number'); + expect(storeEvent.storage).to.be.a('number'); + expect(storeEvent.farmer) + .to.equal('c5857d99d3ff951701093b75fba94e1c82877f9b'); + expect(storeEvent.client).to.equal('user@gmail.com'); + expect(storeEvent.processed).to.equal(false); + done(); + }); + }); + + it('should save storage event with processed true', function(done) { + var event = new StorageEvent({ + token: 'e09958405f0484f6ae54618f48c42ca365a922e9', + user: 'user@gmail.com', + downloadBandwidth: 0, + storage: 1000001, + farmer: '90310d438d3fa6e98082b9431eaf7a82be8a34c2', + client: 'user@gmail.com', + processed: true + }); + + event.save(function(err, storeEvent) { + if (err) { + return done(err); + } + expect(storeEvent.processed).to.equal(true); done(); }); }); diff --git a/test/user.unit.js b/test/user.unit.js index d95629f8..dc3abd56 100644 --- a/test/user.unit.js +++ b/test/user.unit.js @@ -218,6 +218,272 @@ describe('Storage/models/User', function() { }); + describe('#exceedsUnknownReportsThreshold', function() { + it('it return false if reports are unknown', function(done) { + var user = new User({ + _id: 'threshold1@user.tld', + hashpass: '11f8ae09e85636aa47f81ec634a0d977831a3fb0d04b219940bab270b' + + 'a4666cb' + }); + user.save((err) => { + if (err) { + return done(err); + } + + const status = user.exceedsUnknownReportsThreshold(0.4); + expect(status).to.equal(false); + done(); + }); + }); + + it('will return true if reports exceed threshold', function(done) { + const now = new Date(); + const last = new Date(now.getTime() - 10000); + var user = new User({ + _id: 'threshold2@user.tld', + hashpass: '11f8ae09e85636aa47f81ec634a0d977831a3fb0d04b219940bab270b' + + 'a4666cb', + reports: { + totalRate: 1000, + timestamp: last, + unknownRate: 2499 + } + }); + user.save((err) => { + if (err) { + return done(err); + } + const status = user.exceedsUnknownReportsThreshold(0.4); + expect(status).to.equal(true); + done(); + }); + }); + + it('will return false if reports do not exceed threshold', function(done) { + const now = new Date(); + const last = new Date(now.getTime() - 10000); + var user = new User({ + _id: 'threshold3@user.tld', + hashpass: '11f8ae09e85636aa47f81ec634a0d977831a3fb0d04b219940bab270b' + + 'a4666cb', + reports: { + totalRate: 1000, + timestamp: last, + unknownRate: 2500 + } + }); + user.save((err) => { + if (err) { + return done(err); + } + + const status = user.exceedsUnknownReportsThreshold(0.4); + expect(status).to.equal(false); + done(); + }); + }); + }); + + describe('#updateUnknownReports', function() { + it('it will update report rates without pre-existing data', function(done) { + var user = new User({ + _id: 'testreporter1@user.tld', + hashpass: '11f8ae09e85636aa47f81ec634a0d977831a3fb0d04b219940bab270b' + + 'a4666cb' + }); + user.save((err) => { + if (err) { + return done(err); + } + + const now = new Date(); + + user.updateUnknownReports(false, now, 100000000, (err) => { + if (err) { + return done(err); + } + + User.findOne({ _id: 'testreporter1@user.tld' }, function(err, user) { + if (err) { + return done(err); + } + expect(user.reports); + expect(user.reports.totalRate); + expect(user.reports.timestamp); + done(); + }); + }); + }); + }); + + it('it will update total rate and unknown rate equally', function(done) { + const now = new Date(); + const then = new Date(now.getTime() + 10000); + const last = new Date(now.getTime() - 10000); + + var user = new User({ + _id: 'testreporter5@user.tld', + hashpass: '5f018f89c7e37fe45c1dee228750415b299f30612ad0880701960405a' + + '922b684', + reports: { + totalRate: 0, + timestamp: last, + unknownRate: 0 + } + }); + user.save((err) => { + if (err) { + return done(err); + } + + const bytes = 100000000; + + user.updateUnknownReports(false, now, bytes, (err) => { + if (err) { + return done(err); + } + + User.findOne({ _id: 'testreporter5@user.tld' }, function(err, user) { + if (err) { + return done(err); + } + + user.updateUnknownReports(true, then, bytes, (err) => { + if (err) { + return done(err); + } + User.findOne({ + _id: 'testreporter5@user.tld' + }, function(err, user) { + if (err) { + return done(err); + } + expect(user.reports.unknownRate.toFixed(4)).to.equal('3.9992'); + expect(user.reports.totalRate.toFixed(4)).to.equal('7.9968'); + done(); + }); + }); + }); + }); + }); + }); + + it('it will update report rates with unknown status', function(done) { + const now = new Date(); + const last = new Date(now.getTime() - 10000); + + var user = new User({ + _id: 'testreporter2@user.tld', + hashpass: '5f018f89c7e37fe45c1dee228750415b299f30612ad0880701960405a' + + '922b684', + reports: { + totalRate: 4000, + timestamp: last, + unknownRate: 15000, + } + }); + user.save((err) => { + if (err) { + return done(err); + } + + user.updateUnknownReports(true, now, 100000000, (err) => { + if (err) { + return done(err); + } + + User.findOne({ _id: 'testreporter2@user.tld' }, function(err, user) { + if (err) { + return done(err); + } + expect(user.reports.totalRate).to.be.above(4000); + expect(user.reports.timestamp).to.eql(now); + expect(user.reports.unknownRate).to.be.below(15000); + done(); + }); + }); + }); + }); + + it('will not update if timestamp less than current', function(done) { + const now = new Date(); + const then = new Date(now.getTime() + 10000); + + var user = new User({ + _id: 'testreporter3@user.tld', + hashpass: '5f018f89c7e37fe45c1dee228750415b299f30612ad0880701960405' + + 'a922b684', + reports: { + totalRate: 4000, + timestamp: then, + unknownRate: 10000 + } + }); + user.save((err) => { + if (err) { + return done(err); + } + + user.updateUnknownReports(true, now, 100000000, (err) => { + if (err) { + return done(err); + } + + User.findOne({ _id: 'testreporter3@user.tld' }, function(err, user) { + if (err) { + return done(err); + } + expect(user.reports.totalRate).to.equal(4000); + expect(user.reports.timestamp).to.eql(then); + expect(user.reports.unknownRate).to.equal(10000); + done(); + }); + }); + }); + + }); + + + it('it will update timestamp for both report status', function(done) { + const now = new Date(); + const last = new Date(now.getTime() - 10000); + + var user = new User({ + _id: 'testreporter4@user.tld', + hashpass: '5f018f89c7e37fe45c1dee228750415b299f30612ad0880701960405' + + 'a922b684', + reports: { + totalRate: 4000, + timestamp: last, + unknownRate: 15000 + } + }); + user.save((err) => { + if (err) { + return done(err); + } + + user.updateUnknownReports(false, now, 100000000, (err) => { + if (err) { + return done(err); + } + + User.findOne({ _id: 'testreporter4@user.tld' }, function(err, user) { + if (err) { + return done(err); + } + expect(user.reports.totalRate).to.be.above(4000); + expect(user.reports.timestamp).to.eql(now); + expect(user.reports.unknownRate).to.equal(15000); + done(); + }); + }); + }); + }); + + }); + + describe('#recordDownloadBytes', function() { it('should record the bytes and increment existing', function(done) { var user = new User({