}
remote.remoteVideosAdd = function (req, res, next) {
- req.checkBody('data.name', 'Should have a name').isLength(1, 50)
- req.checkBody('data.description', 'Should have a description').isLength(1, 250)
- req.checkBody('data.magnetUri', 'Should have a magnetUri').notEmpty()
- req.checkBody('data.podUrl', 'Should have a podUrl').isURL()
+ req.checkBody('data').isArray()
+ req.checkBody('data').eachIsRemoteVideosAddValid()
logger.debug('Checking remoteVideosAdd parameters', { parameters: req.body })
}
remote.remoteVideosRemove = function (req, res, next) {
- req.checkBody('data.magnetUri', 'Should have a magnetUri').notEmpty()
+ req.checkBody('data').isArray()
+ req.checkBody('data').eachIsRemoteVideosRemoveValid()
logger.debug('Checking remoteVideosRemove parameters', { parameters: req.body })
"jquery": "^2.1.4",
"js-yaml": "^3.3.1",
"load-grunt-tasks": "^3.3.0",
+ "lodash-node": "3.10.1",
"mkdirp": "^0.5.1",
"mongoose": "^4.0.5",
"morgan": "^1.5.3",
"segfault-handler": "^0.2.4",
"time-grunt": "^1.2.1",
"ursa": "^0.9.1",
+ "validator": "^4.3.0",
"webtorrent": "*",
"winston": "^1.0.1",
"ws": "^0.8.0"
var express = require('express')
var router = express.Router()
+ var pluck = require('lodash-node/compat/collection/pluck')
+
var middleware = require('../../../middlewares')
var miscMiddleware = middleware.misc
var reqValidator = middleware.reqValidators.remote
var videos = require('../../../src/videos')
function addRemoteVideos (req, res, next) {
- videos.addRemote(req.body.data, function (err, video) {
+ videos.addRemotes(req.body.data, function (err, videos) {
if (err) return next(err)
- res.json(video)
+ res.json(videos)
})
}
function removeRemoteVideo (req, res, next) {
- videos.removeRemote(req.body.signature.url, req.body.data.magnetUri, function (err) {
+ videos.removeRemotes(req.body.signature.url, pluck(req.body.data, 'magnetUri'), function (err) {
if (err) return next(err)
res.status(204)
// ----------- PeerTube modules -----------
var config = require('config')
+ var customValidators = require('./src/customValidators')
var logger = require('./src/logger')
+ var poolRequests = require('./src/poolRequests')
var routes = require('./routes')
var videos = require('./src/videos')
var webtorrent = require('./src/webTorrentNode')
app.use(multer({ dest: uploads }))
app.use(bodyParser.urlencoded({ extended: false }))
// Validate some params for the API
- app.use(expressValidator())
+ app.use(expressValidator({
+ customValidators: customValidators
+ }))
// ----------- Views, routes and static files -----------
// ----------- Make the server listening -----------
server.listen(port, function () {
+ // Activate the pool requests
+ poolRequests.activate()
+
videos.seedAll(function () {
logger.info('Seeded all the videos')
logger.info('Server listening on port %d', port)
--- /dev/null
+;(function () {
+ 'use strict'
+
+ var validator = require('validator')
+
+ var customValidators = {}
+
+ customValidators.eachIsRemoteVideosAddValid = function (values) {
+ return values.every(function (val) {
+ return validator.isLength(val.name, 1, 50) &&
+ validator.isLength(val.description, 1, 50) &&
+ validator.isLength(val.magnetUri, 10) &&
+ validator.isURL(val.podUrl)
+ })
+ }
+
+ customValidators.eachIsRemoteVideosRemoveValid = function (values) {
+ return values.every(function (val) {
+ return validator.isLength(val.magnetUri, 10)
+ })
+ }
+
+ customValidators.isArray = function (value) {
+ return Array.isArray(value)
+ }
+
+ // ----------- Export -----------
+ module.exports = customValidators
+})()
var PodsDB = mongoose.model('pods', podsSchema)
+ // ----------- PoolRequests -----------
+ var poolRequestsSchema = mongoose.Schema({
+ type: String,
+ id: String, // Special id to find duplicates (video created we want to remove...)
+ request: mongoose.Schema.Types.Mixed
+ })
+
+ var PoolRequestsDB = mongoose.model('poolRequests', poolRequestsSchema)
+
// ----------- Connection -----------
mongoose.connect('mongodb://' + host + ':' + port + '/' + dbname)
// ----------- Export -----------
module.exports = {
VideosDB: VideosDB,
- PodsDB: PodsDB
+ PodsDB: PodsDB,
+ PoolRequestsDB: PoolRequestsDB
}
})()
var logger = require('./logger')
var PodsDB = require('./database').PodsDB
+ var poolRequests = require('./poolRequests')
var utils = require('./utils')
var pods = {}
var host = config.get('webserver.host')
var port = config.get('webserver.port')
- // ----------- Constants -----------
-
- var PODS_SCORE = {
- MALUS: -10,
- BONUS: 10
- }
-
// ----------- Private functions -----------
function getForeignPodsList (url, callback) {
})
}
- function updatePodsScore (good_pods, bad_pods) {
- logger.info('Updating %d good pods and %d bad pods scores.', good_pods.length, bad_pods.length)
-
- PodsDB.update({ _id: { $in: good_pods } }, { $inc: { score: PODS_SCORE.BONUS } }, { multi: true }).exec()
- PodsDB.update({ _id: { $in: bad_pods } }, { $inc: { score: PODS_SCORE.MALUS } }, { multi: true }, function (err) {
- if (err) throw err
- removeBadPods()
- })
- }
-
- function removeBadPods () {
- PodsDB.remove({ score: 0 }, function (err, result) {
- if (err) throw err
-
- var number_removed = result.result.n
- if (number_removed !== 0) logger.info('Removed %d pod.', number_removed)
- })
- }
-
// ----------- Public functions -----------
pods.list = function (callback) {
})
}
- // { path, data }
- pods.makeSecureRequest = function (data, callback) {
- if (callback === undefined) callback = function () {}
-
- PodsDB.find({}, { _id: 1, url: 1, publicKey: 1 }).exec(function (err, pods) {
- if (err) {
- logger.error('Cannot get the list of the pods.', { error: err })
- return callback(err)
- }
-
- logger.debug('Make multiple requests.')
-
- var params = {
- encrypt: true,
- sign: true,
- method: data.method,
- path: data.path,
- data: data.data
- }
-
- var bad_pods = []
- var good_pods = []
-
- utils.makeMultipleRetryRequest(
- params,
-
- pods,
-
- function callbackEachPodFinished (err, response, body, pod, callback_each_pod_finished) {
- if (err || response.statusCode !== 200) {
- bad_pods.push(pod._id)
- logger.error('Error sending secure request to %s/%s pod.', pod.url, data.path, { error: err })
- } else {
- good_pods.push(pod._id)
- }
-
- return callback_each_pod_finished()
- },
-
- function callbackAllPodsFinished (err) {
- if (err) {
- logger.error('There was some errors when sending the video meta data.', { error: err })
- return callback(err)
- }
-
- logger.debug('Finished')
+ pods.addVideoToFriends = function (video) {
+ // To avoid duplicates
+ var id = video.name + video.magnetUri
+ poolRequests.addToPoolRequests(id, 'add', video)
+ }
- updatePodsScore(good_pods, bad_pods)
- callback(null)
- }
- )
- })
+ pods.removeVideoToFriends = function (video) {
+ // To avoid duplicates
+ var id = video.name + video.magnetUri
+ poolRequests.addToPoolRequests(id, 'remove', video)
}
pods.makeFriends = function (callback) {
pods_list,
- function eachRequest (err, response, body, pod, callback_each_request) {
+ function eachRequest (err, response, body, url, pod, callback_each_request) {
// We add the pod if it responded correctly with its public certificate
if (!err && response.statusCode === 200) {
pods.add({ url: pod.url, publicKey: body.cert, score: global.FRIEND_BASE_SCORE }, function (err) {
--- /dev/null
+;(function () {
+ 'use strict'
+
+ var async = require('async')
+
+ var logger = require('./logger')
+ var database = require('./database')
+ var PoolRequestsDB = database.PoolRequestsDB
+ var PodsDB = database.PodsDB
+ var utils = require('./utils')
+
+ var poolRequests = {}
+
+ // ----------- Constants -----------
+
+ // Time to wait between requests to the friends
+ var INTERVAL = utils.isTestInstance() ? 10000 : 60000
+ var PODS_SCORE = {
+ MALUS: -10,
+ BONUS: 10
+ }
+
+ // ----------- Private -----------
+ var timer = null
+
+ function makePoolRequests () {
+ logger.info('Making pool requests to friends.')
+
+ PoolRequestsDB.find({}, { type: 1, request: 1 }, function (err, pool_requests) {
+ if (err) throw err
+
+ var requests = {
+ add: [],
+ remove: []
+ }
+
+ async.each(pool_requests, function (pool_request, callback_each) {
+ if (pool_request.type === 'add') {
+ requests.add.push(pool_request.request)
+ } else if (pool_request.type === 'remove') {
+ requests.remove.push(pool_request.request)
+ } else {
+ throw new Error('Unkown pool request type.')
+ }
+
+ callback_each()
+ }, function () {
+ makePoolRequest('add', requests.add)
+ makePoolRequest('remove', requests.remove)
+ logger.info('Pool requests to friends sent.')
+ })
+ })
+ }
+
+ function updatePodsScore (good_pods, bad_pods) {
+ logger.info('Updating %d good pods and %d bad pods scores.', good_pods.length, bad_pods.length)
+
+ PodsDB.update({ _id: { $in: good_pods } }, { $inc: { score: PODS_SCORE.BONUS } }, { multi: true }).exec()
+ PodsDB.update({ _id: { $in: bad_pods } }, { $inc: { score: PODS_SCORE.MALUS } }, { multi: true }, function (err) {
+ if (err) throw err
+ removeBadPods()
+ })
+ }
+
+ function removeBadPods () {
+ PodsDB.remove({ score: 0 }, function (err, result) {
+ if (err) throw err
+
+ var number_removed = result.result.n
+ if (number_removed !== 0) logger.info('Removed %d pod.', number_removed)
+ })
+ }
+
+ function makePoolRequest (type, requests) {
+ logger.debug('Make pool requests scheduled.')
+ PodsDB.find({}, { _id: 1, url: 1, publicKey: 1 }).exec(function (err, pods) {
+ if (err) throw err
+
+ var params = {
+ encrypt: true,
+ sign: true,
+ method: 'POST',
+ path: null,
+ data: requests
+ }
+
+ if (type === 'add') {
+ params.path = '/api/' + global.API_VERSION + '/remotevideos/add'
+ } else if (type === 'remove') {
+ params.path = '/api/' + global.API_VERSION + '/remotevideos/remove'
+ } else {
+ throw new Error('Unkown pool request type.')
+ }
+
+ var bad_pods = []
+ var good_pods = []
+
+ utils.makeMultipleRetryRequest(params, pods, callbackEachPodFinished, callbackAllPodsFinished)
+
+ function callbackEachPodFinished (err, response, body, url, pod, callback_each_pod_finished) {
+ if (err || response.statusCode !== 200) {
+ bad_pods.push(pod._id)
+ logger.error('Error sending secure request to %s pod.', url, { error: err })
+ } else {
+ good_pods.push(pod._id)
+ }
+
+ return callback_each_pod_finished()
+ }
+
+ function callbackAllPodsFinished (err) {
+ if (err) {
+ logger.error('There was some errors when sending the video meta data.', { error: err })
+ }
+
+ updatePodsScore(good_pods, bad_pods)
+ PoolRequestsDB.remove().exec()
+ }
+ })
+ }
+
+ // ----------- Public -----------
+ poolRequests.activate = function () {
+ logger.info('Pool requests activated.')
+ timer = setInterval(makePoolRequests, INTERVAL)
+ }
+
+ poolRequests.addToPoolRequests = function (id, type, request) {
+ logger.debug('Add request to the pool requests.', { id: id, type: type, request: request })
+
+ PoolRequestsDB.findOne({ id: id }, function (err, entity) {
+ if (err) logger.error(err)
+
+ if (entity) {
+ if (entity.type === type) {
+ logger.error(new Error('Cannot insert two same requests.'))
+ return
+ }
+
+ // Remove the request of the other type
+ PoolRequestsDB.remove({ id: id }, function (err) {
+ if (err) logger.error(err)
+ })
+ } else {
+ PoolRequestsDB.create({ id: id, type: type, request: request }, function (err) {
+ if (err) logger.error(err)
+ })
+ }
+ })
+ }
+
+ poolRequests.deactivate = function () {
+ logger.info('Pool requests deactivated.')
+ clearInterval(timer)
+ }
+
+ module.exports = poolRequests
+})()
replay(
request.post(params, function (err, response, body) {
- callbackEach(err, response, body, to_pod)
+ callbackEach(err, response, body, params.url, to_pod)
}),
{
retries: retries,
// Make a request for each pod
async.each(pods, function (pod, callback_each_async) {
- function callbackEachRetryRequest (err, response, body, pod) {
- callbackEach(err, response, body, pod, function () {
+ function callbackEachRetryRequest (err, response, body, url, pod) {
+ callbackEach(err, response, body, url, pod, function () {
callback_each_async()
})
}
var async = require('async')
var config = require('config')
+ var dz = require('dezalgo')
var fs = require('fs')
var webtorrent = require('./webTorrentNode')
return callback(err)
}
- // Now we'll send the video's meta data
+ // Now we'll add the video's meta data to our friends
params.namePath = null
- logger.info('Sending %s video to friends.', video_file.path)
-
- var data = {
- path: '/api/' + global.API_VERSION + '/remotevideos/add',
- method: 'POST',
- data: params
- }
-
- // Do not wait the secure requests
- pods.makeSecureRequest(data)
+ pods.addVideoToFriends(params)
callback(null)
})
})
return callback(err)
}
- var data = {
- path: '/api/' + global.API_VERSION + '/remotevideos/remove',
- method: 'POST',
- data: {
- magnetUri: video.magnetUri
- }
+ var params = {
+ name: video.name,
+ magnetUri: video.magnetUri
}
- // Yes this is a POST request because we add some informations in the body (signature, encrypt etc)
- pods.makeSecureRequest(data)
+ pods.removeVideoToFriends(params)
callback(null)
})
})
}
// Use the magnet Uri because the _id field is not the same on different servers
- videos.removeRemote = function (fromUrl, magnetUri, callback) {
- VideosDB.findOne({ magnetUri: magnetUri }, function (err, video) {
- if (err || !video) {
- logger.error('Cannot find the torrent URI of this remote video.')
+ videos.removeRemotes = function (fromUrl, magnetUris, callback) {
+ VideosDB.find({ magnetUri: { $in: magnetUris } }, function (err, videos) {
+ if (err || !videos) {
+ logger.error('Cannot find the torrent URI of these remote videos.')
return callback(err)
}
- // TODO: move to reqValidators middleware ?
- if (video.podUrl !== fromUrl) {
- logger.error('The pod has not the rights on this video.')
- return callback(err)
- }
+ var to_remove = []
+ async.each(videos, function (video, callback_async) {
+ callback_async = dz(callback_async)
- VideosDB.findByIdAndRemove(video._id, function (err) {
- if (err) {
- logger.error('Cannot remove the remote video.')
- return callback(err)
+ if (video.podUrl !== fromUrl) {
+ logger.error('The pod %s has not the rights on the video of %s.', fromUrl, video.podUrl)
+ } else {
+ to_remove.push(video._id)
}
- callback(null)
+ callback_async()
+ }, function () {
+ VideosDB.remove({ _id: { $in: to_remove } }, function (err) {
+ if (err) {
+ logger.error('Cannot remove the remote videos.')
+ return callback(err)
+ }
+
+ callback(null)
+ })
})
})
}
// { name, magnetUri, podUrl }
- videos.addRemote = function (data, callback) {
- logger.debug('Add remote video from pod: %s', data.podUrl)
-
- var params = {
- name: data.name,
- namePath: null,
- description: data.description,
- magnetUri: data.magnetUri,
- podUrl: data.podUrl
- }
+ videos.addRemotes = function (videos, callback) {
+ var to_add = []
- VideosDB.create(params, function (err, video) {
- if (err) {
- logger.error('Cannot insert this remote video.', { error: err })
- return callback(err)
+ async.each(videos, function (video, callback_each) {
+ callback_each = dz(callback_each)
+ logger.debug('Add remote video from pod: %s', video.podUrl)
+
+ var params = {
+ name: video.name,
+ namePath: null,
+ description: video.description,
+ magnetUri: video.magnetUri,
+ podUrl: video.podUrl
}
- return callback(null, video)
+ to_add.push(params)
+
+ callback_each()
+ }, function () {
+ VideosDB.create(to_add, function (err, videos) {
+ if (err) {
+ logger.error('Cannot insert this remote video.', { error: err })
+ return callback(err)
+ }
+
+ return callback(null, videos)
+ })
})
}
})
it('Should make friends with the pods 1, 2, 3', function (done) {
- this.timeout(100000)
+ this.timeout(150000)
- // Pods 1, 2, 3 and 4 become friends
+ // Pods 1, 2, 3 and 4 become friends (yes this is beautiful)
makeFriend(2, function () {
makeFriend(1, function () {
makeFriend(4, function () {
// Expulse pod 4 from pod 1 and 2
uploadVideo(1, function () {
- uploadVideo(1, function () {
- uploadVideo(2, function () {
- uploadVideo(2, function () {
- // Rerun server 4
- utils.runServer(4, function (app, url) {
- apps[3] = app
- getFriendsList(4, function (err, res) {
- if (err) throw err
- // Pod 4 didn't know pod 1 and 2 removed it
- expect(res.body.length).to.equal(3)
-
- // Pod 6 ask pod 1, 2 and 3
- makeFriend(6, function () {
- getFriendsList(6, function (err, res) {
- if (err) throw err
-
- // Pod 4 should not be our friend
- var result = res.body
- expect(result.length).to.equal(3)
- for (var pod of result) {
- expect(pod.url).not.equal(urls[3])
- }
-
- done()
- })
+ setTimeout(function () {
+ uploadVideo(1, function () {
+ setTimeout(function () {
+ uploadVideo(2, function () {
+ setTimeout(function () {
+ uploadVideo(2, function () {
+ setTimeout(function () {
+ // Rerun server 4
+ utils.runServer(4, function (app, url) {
+ apps[3] = app
+ getFriendsList(4, function (err, res) {
+ if (err) throw err
+ // Pod 4 didn't know pod 1 and 2 removed it
+ expect(res.body.length).to.equal(3)
+
+ // Pod 6 ask pod 1, 2 and 3
+ makeFriend(6, function () {
+ getFriendsList(6, function (err, res) {
+ if (err) throw err
+
+ // Pod 4 should not be our friend
+ var result = res.body
+ expect(result.length).to.equal(3)
+ for (var pod of result) {
+ expect(pod.url).not.equal(urls[3])
+ }
+
+ done()
+ })
+ })
+ })
+ })
+ }, 11000)
})
- })
+ }, 11000)
})
- })
+ }, 11000)
})
- })
+ }, 11000)
})
})
})
var path = '/api/v1/videos'
var apps = []
var urls = []
- var video_id = -1
+ var to_remove = []
function getVideosList (url, end) {
request(url)
.end(end)
}
+ function removeVideo (url, id, end) {
+ request(url)
+ .delete(path + '/' + id)
+ .set('Accept', 'application/json')
+ .expect(204)
+ .end(end)
+ }
+
before(function (done) {
this.timeout(30000)
var path_friends = '/api/v1/pods/makefriends'
describe('Should upload the video and propagate on each pod', function () {
it('Should upload the video on pod 1 and propagate on each pod', function (done) {
- this.timeout(5000)
+ this.timeout(15000)
uploadVideo(urls[0], 'my super name for pod 1', 'my super description for pod 1', 'video_short1.webm', function (err) {
if (err) throw err
done()
})
- }, 1000)
+ }, 11000)
})
})
it('Should upload the video on pod 2 and propagate on each pod', function (done) {
- this.timeout(5000)
+ this.timeout(15000)
uploadVideo(urls[1], 'my super name for pod 2', 'my super description for pod 2', 'video_short2.webm', function (err) {
if (err) throw err
done()
})
- }, 1000)
+ }, 11000)
})
})
- it('Should upload the video on pod 3 and propagate on each pod', function (done) {
- this.timeout(5000)
+ it('Should upload two videos on pod 3 and propagate on each pod', function (done) {
+ this.timeout(15000)
uploadVideo(urls[2], 'my super name for pod 3', 'my super description for pod 3', 'video_short3.webm', function (err) {
if (err) throw err
+ uploadVideo(urls[2], 'my super name for pod 3-2', 'my super description for pod 3-2', 'video_short.webm', function (err) {
+ if (err) throw err
- setTimeout(function () {
- var base_magnet = null
- // All pods should have this video
- async.each(urls, function (url, callback) {
- getVideosList(url, function (err, res) {
- if (err) throw err
-
- var videos = res.body
- expect(videos).to.be.an('array')
- expect(videos.length).to.equal(3)
- var video = videos[2]
- expect(video.name).to.equal('my super name for pod 3')
- expect(video.description).to.equal('my super description for pod 3')
- expect(video.podUrl).to.equal('http://localhost:9003')
- expect(video.magnetUri).to.exist
+ setTimeout(function () {
+ var base_magnet = null
+ // All pods should have this video
+ async.each(urls, function (url, callback) {
+ getVideosList(url, function (err, res) {
+ if (err) throw err
- // All pods should have the same magnet Uri
- if (base_magnet === null) {
- base_magnet = video.magnetUri
- } else {
- expect(video.magnetUri).to.equal.magnetUri
- }
+ var videos = res.body
+ expect(videos).to.be.an('array')
+ expect(videos.length).to.equal(4)
+ var video = videos[2]
+ expect(video.name).to.equal('my super name for pod 3')
+ expect(video.description).to.equal('my super description for pod 3')
+ expect(video.podUrl).to.equal('http://localhost:9003')
+ expect(video.magnetUri).to.exist
+
+ video = videos[3]
+ expect(video.name).to.equal('my super name for pod 3-2')
+ expect(video.description).to.equal('my super description for pod 3-2')
+ expect(video.podUrl).to.equal('http://localhost:9003')
+ expect(video.magnetUri).to.exist
+
+ // All pods should have the same magnet Uri
+ if (base_magnet === null) {
+ base_magnet = video.magnetUri
+ } else {
+ expect(video.magnetUri).to.equal.magnetUri
+ }
+
+ callback()
+ })
+ }, function (err) {
+ if (err) throw err
- callback()
+ done()
})
- }, function (err) {
- if (err) throw err
-
- done()
- })
- }, 1000)
+ }, 11000)
+ })
})
})
})
if (err) throw err
var video = res.body[0]
+ to_remove.push(res.body[2]._id)
+ to_remove.push(res.body[3]._id)
+
webtorrent.add(video.magnetUri, function (torrent) {
expect(torrent.files).to.exist
expect(torrent.files.length).to.equal(1)
if (err) throw err
var video = res.body[2]
- video_id = res.body[1]._id
webtorrent.add(video.magnetUri, function (torrent) {
expect(torrent.files).to.exist
})
})
- it('Should remove the file 2 by asking pod 2', function (done) {
- request(urls[1])
- .delete(path + '/' + video_id)
- .set('Accept', 'application/json')
- .expect(204)
- .end(function (err, res) {
+ it('Should add the file 3-2 by asking pod 1', function (done) {
+ // Yes, this could be long
+ this.timeout(200000)
+
+ getVideosList(urls[0], function (err, res) {
+ if (err) throw err
+
+ var video = res.body[3]
+
+ webtorrent.add(video.magnetUri, function (torrent) {
+ expect(torrent.files).to.exist
+ expect(torrent.files.length).to.equal(1)
+ expect(torrent.files[0].path).to.exist.and.to.not.equal('')
+
+ done()
+ })
+ })
+ })
+
+ it('Should remove the file 3 and 3-2 by asking pod 3', function (done) {
+ this.timeout(15000)
+
+ removeVideo(urls[2], to_remove[0], function (err) {
+ if (err) throw err
+ removeVideo(urls[2], to_remove[1], function (err) {
if (err) throw err
// Wait the propagation to the other pods
setTimeout(function () {
done()
- }, 1000)
+ }, 11000)
})
+ })
})
it('Should have videos 1 and 3 on each pod', function (done) {
expect(videos).to.be.an('array')
expect(videos.length).to.equal(2)
expect(videos[0]._id).not.to.equal(videos[1]._id)
- expect(videos[0]._id).not.to.equal(video_id)
- expect(videos[1]._id).not.to.equal(video_id)
+ expect(videos[0]._id).not.to.equal(to_remove[0])
+ expect(videos[1]._id).not.to.equal(to_remove[0])
+ expect(videos[0]._id).not.to.equal(to_remove[1])
+ expect(videos[1]._id).not.to.equal(to_remove[1])
callback()
})