add tests for storage backends

This commit is contained in:
Girish Ramakrishnan
2017-10-02 20:08:00 -07:00
parent 7bf70956a1
commit 21afc71d89
8 changed files with 327 additions and 261 deletions
+3 -1
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:progress');
// if progress.update or progress.backup are object, they will contain 'percent' and 'message' properties
@@ -41,6 +42,8 @@ function setDetail(tag, detail) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof detail, 'string');
if (config.TEST && !progress[tag]) progress[tag] = { };
progress[tag].detail = detail;
}
@@ -55,4 +58,3 @@ function clear(tag) {
function getAll() {
return progress;
}
+4 -4
View File
@@ -86,7 +86,7 @@ function downloadDir(apiConfig, backupFilePath, destDir, callback) {
shell.exec('downloadDir', '/bin/cp', [ '-r', backupFilePath + '/.', destDir ], { }, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
callback();
callback(null);
});
}
@@ -105,7 +105,7 @@ function copy(apiConfig, oldFilePath, newFilePath, callback) {
shell.exec('copy', '/bin/cp', [ '-al', oldFilePath, newFilePath ], { }, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
callback();
callback(null);
});
});
}
@@ -124,7 +124,7 @@ function remove(apiConfig, filename, callback) {
if (!safe.fs.rmdirSync(filename)) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, safe.error.message));
}
callback();
callback(null);
}
function removeDir(apiConfig, pathPrefix, callback) {
@@ -135,7 +135,7 @@ function removeDir(apiConfig, pathPrefix, callback) {
shell.exec('removeDir', '/bin/rm', [ '-rf', pathPrefix ], { }, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
callback();
callback(null);
});
}
+7 -7
View File
@@ -25,7 +25,7 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
debug('upload: %s', backupFilePath);
callback();
callback(null);
}
function download(apiConfig, backupFilePath, callback) {
@@ -46,7 +46,7 @@ function downloadDir(apiConfig, backupFilePath, destDir, callback) {
debug('downloadDir: %s -> %s', backupFilePath, destDir);
callback();
callback(new Error('Cannot download from noop backend'));
}
function copy(apiConfig, oldFilePath, newFilePath, callback) {
@@ -57,7 +57,7 @@ function copy(apiConfig, oldFilePath, newFilePath, callback) {
debug('copy: %s -> %s', oldFilePath, newFilePath);
callback();
callback(null);
}
function remove(apiConfig, filename, callback) {
@@ -67,7 +67,7 @@ function remove(apiConfig, filename, callback) {
debug('remove: %s', filename);
callback();
callback(null);
}
function removeDir(apiConfig, pathPrefix, callback) {
@@ -77,14 +77,14 @@ function removeDir(apiConfig, pathPrefix, callback) {
debug('removeDir: %s', pathPrefix);
callback();
callback(null);
}
function testConfig(apiConfig, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof callback, 'function');
callback();
callback(null);
}
function backupDone(apiConfig, backupId, appBackupIds, callback) {
@@ -93,5 +93,5 @@ function backupDone(apiConfig, backupId, appBackupIds, callback) {
assert(Array.isArray(appBackupIds));
assert.strictEqual(typeof callback, 'function');
callback();
callback(null);
}
+1 -1
View File
@@ -189,7 +189,7 @@ function listDir(apiConfig, backupFilePath, options, iteratorCallback, callback)
});
});
}, function (error) {
if (error.message === 'Done') return callback();
if (error.message === 'Done') return callback(null);
callback(error);
});
+140 -1
View File
@@ -15,9 +15,80 @@ var async = require('async'),
expect = require('expect.js'),
fs = require('fs'),
os = require('os'),
mkdirp = require('mkdirp'),
readdirp = require('readdirp'),
path = require('path'),
progress = require('../progress.js'),
rimraf = require('rimraf'),
settings = require('../settings.js');
settings = require('../settings.js'),
SettingsError = require('../settings.js').SettingsError;
function compareDirectories(one, two, callback) {
readdirp({ root: one }, function (error, treeOne) {
if (error) return callback(error);
readdirp({ root: two }, function (error, treeTwo) {
if (error) return callback(error);
var mismatch = [];
function compareDirs(a, b) {
a.forEach(function (tmpA) {
var found = b.find(function (tmpB) {
return tmpA.path === tmpB.path;
});
if (!found) mismatch.push(tmpA);
});
}
function compareFiles(a, b) {
a.forEach(function (tmpA) {
var found = b.find(function (tmpB) {
// TODO check file or symbolic link
return tmpA.path === tmpB.path && tmpA.mode === tmpB.mode;
});
if (!found) mismatch.push(tmpA);
});
}
compareDirs(treeOne.directories, treeTwo.directories);
compareDirs(treeTwo.directories, treeOne.directories);
compareFiles(treeOne.files, treeTwo.files);
compareFiles(treeTwo.files, treeOne.files);
if (mismatch.length) {
console.error('Files not found in both: %j', mismatch);
return callback(new Error('file mismatch'));
}
callback(null);
});
});
}
function createBackup(callback) {
backups.backup({ username: 'test' }, function (error) { // this call does not wait for the backup!
if (error) return callback(error);
function waitForBackup() {
var p = progress.getAll();
if (p.backup.percent !== 100) return setTimeout(waitForBackup, 1000);
if (p.backup.message) return callback(new Error('backup failed:' + p.backup.message));
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, result) {
if (error) return callback(error);
if (result.length !== 1) return callback(new Error('result is not of length 1'));
callback(null, result[0]);
});
}
setTimeout(waitForBackup, 1000);
});
}
describe('backups', function () {
before(function (done) {
@@ -209,4 +280,72 @@ describe('backups', function () {
});
});
});
describe('filesystem', function () {
var backupInfo1;
var gBackupConfig = {
provider: 'filesystem',
backupFolder: path.join(os.tmpdir(), 'backups-test-filesystem'),
format: 'tgz'
};
before(function (done) {
rimraf.sync(gBackupConfig.backupFolder);
done();
});
after(function (done) {
rimraf.sync(gBackupConfig.backupFolder);
progress.clear(progress.BACKUP);
done();
});
it('fails to set backup config for non-existing folder', function (done) {
settings.setBackupConfig(gBackupConfig, function (error) {
expect(error).to.be.a(SettingsError);
expect(error.reason).to.equal(SettingsError.BAD_FIELD);
done();
});
});
it('succeeds to set backup config', function (done) {
mkdirp.sync(gBackupConfig.backupFolder);
settings.setBackupConfig(gBackupConfig, function (error) {
expect(error).to.be(null);
done();
});
});
it('can backup', function (done) {
this.timeout(6000);
createBackup(function (error, result) {
expect(error).to.be(null);
expect(fs.statSync(path.join(gBackupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup
expect(fs.statSync(path.join(gBackupConfig.backupFolder, `${result.id}.tar.gz`)).nlink).to.be(2);
backupInfo1 = result;
done();
});
});
it('can take another backup', function (done) {
this.timeout(6000);
createBackup(function (error, result) {
expect(error).to.be(null);
expect(fs.statSync(path.join(gBackupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup
expect(fs.statSync(path.join(gBackupConfig.backupFolder, `${result.id}.tar.gz`)).nlink).to.be(2); // hard linked to new backup
expect(fs.statSync(path.join(gBackupConfig.backupFolder, `${backupInfo1.id}.tar.gz`)).nlink).to.be(1); // not hard linked anymore
done();
});
});
});
});
+2 -3
View File
@@ -6,12 +6,11 @@
'use strict';
var async = require('async'),
progress = require('../progress.js'),
config = require('../config.js'),
var config = require('../config.js'),
database = require('../database.js'),
expect = require('expect.js'),
nock = require('nock'),
progress = require('../progress.js'),
superagent = require('superagent'),
server = require('../server.js');
+170 -244
View File
@@ -5,100 +5,22 @@
'use strict';
var async = require('async'),
backups = require('../backups.js'),
BackupsError = require('../backups.js').BackupsError,
config = require('../config.js'),
database = require('../database.js'),
var BackupsError = require('../backups.js').BackupsError,
expect = require('expect.js'),
filesystem = require('../storage/filesystem.js'),
fs = require('fs'),
mkdirp = require('mkdirp'),
MockS3 = require('mock-aws-s3'),
noop = require('../storage/noop.js'),
os = require('os'),
path = require('path'),
progress = require('../progress.js'),
readdirp = require('readdirp'),
rimraf = require('rimraf'),
s3 = require('../storage/s3.js'),
settings = require('../settings.js'),
SettingsError = settings.SettingsError;
function setup(done) {
config.set('provider', 'caas');
async.series([
database.initialize,
settings.initialize,
function (callback) {
// a cloudron must have a backup config to startup
settings.setBackupConfig({ provider: 'filesystem', format: 'tgz', backupFolder: '/tmp'}, function (error) {
expect(error).to.be(null);
callback();
});
}
], done);
}
function cleanup(done) {
async.series([
settings.uninitialize,
database._clear
], done);
}
function compareDirectories(one, two, callback) {
readdirp({ root: one }, function (error, treeOne) {
if (error) return callback(error);
readdirp({ root: two }, function (error, treeTwo) {
if (error) return callback(error);
var mismatch = [];
function compareDirs(a, b) {
a.forEach(function (tmpA) {
var found = b.find(function (tmpB) {
return tmpA.path === tmpB.path;
});
if (!found) mismatch.push(tmpA);
});
}
function compareFiles(a, b) {
a.forEach(function (tmpA) {
var found = b.find(function (tmpB) {
// TODO check file or symbolic link
return tmpA.path === tmpB.path && tmpA.mode === tmpB.mode;
});
if (!found) mismatch.push(tmpA);
});
}
compareDirs(treeOne.directories, treeTwo.directories);
compareDirs(treeTwo.directories, treeOne.directories);
compareFiles(treeOne.files, treeTwo.files);
compareFiles(treeTwo.files, treeOne.files);
if (mismatch.length) {
console.error('Files not found in both: %j', mismatch);
return callback(new Error('file mismatch'));
}
callback(null);
});
});
}
s3 = require('../storage/s3.js');
describe('Storage', function () {
describe('filesystem', function () {
var gBackupId_1;
var gBackupId_2;
var gTmpFolder;
var gSourceFolder;
var gDestinationFolder;
var gBackupConfig = {
provider: 'filesystem',
key: 'key',
@@ -107,130 +29,171 @@ describe('Storage', function () {
};
before(function (done) {
setup(function (error) {
expect(error).to.be(null);
gTmpFolder = fs.mkdtempSync(path.join(os.tmpdir(), 'filesystem-storage-test_'));
gTmpFolder = fs.mkdtempSync(path.join(os.tmpdir(), 'filesystem-backup-test_'));
gBackupConfig.backupFolder = path.join(gTmpFolder, 'backups/');
gBackupConfig.backupFolder = path.join(gTmpFolder, 'backups/');
gSourceFolder = path.join(__dirname, 'storage');
gDestinationFolder = path.join(gTmpFolder, 'destination/');
gBackupId_1 = backups._getBackupFilePath(gBackupConfig, 'someprefix/one', gBackupConfig.format);
gBackupId_2 = backups._getBackupFilePath(gBackupConfig, 'someprefix/two', gBackupConfig.format);
done();
});
done();
});
after(function (done) {
cleanup(function (error) {
rimraf.sync(gTmpFolder);
done();
});
it('can upload', function (done) {
var sourceFile = path.join(__dirname, 'storage/data/test.txt');
var sourceStream = fs.createReadStream(sourceFile);
var destFile = gTmpFolder + '/uploadtest/test.txt';
filesystem.upload(gBackupConfig, destFile, sourceStream, function (error) {
expect(error).to.be(null);
rimraf.sync(gTmpFolder);
expect(fs.existsSync(destFile));
expect(fs.statSync(sourceFile).size).to.be(fs.statSync(destFile).size);
done();
});
});
it('fails to set backup config for non-existing folder', function (done) {
settings.setBackupConfig(gBackupConfig, function (error) {
expect(error).to.be.a(SettingsError);
expect(error.reason).to.equal(SettingsError.BAD_FIELD);
it('upload waits for empty file to be created', function (done) {
var sourceFile = path.join(__dirname, 'storage/data/empty');
var sourceStream = fs.createReadStream(sourceFile);
var destFile = gTmpFolder + '/uploadtest/empty';
filesystem.upload(gBackupConfig, destFile, sourceStream, function (error) {
expect(error).to.be(null);
expect(fs.existsSync(destFile));
expect(fs.statSync(sourceFile).size).to.be(fs.statSync(destFile).size);
done();
});
});
it('succeeds to set backup config', function (done) {
mkdirp.sync(gBackupConfig.backupFolder);
settings.setBackupConfig(gBackupConfig, function (error) {
it('upload unlinks old file', function (done) {
var sourceFile = path.join(__dirname, 'storage/data/test.txt');
var sourceStream = fs.createReadStream(sourceFile);
var destFile = gTmpFolder + '/uploadtest/test.txt';
var oldStat = fs.statSync(destFile);
filesystem.upload(gBackupConfig, destFile, sourceStream, function (error) {
expect(error).to.be(null);
expect(fs.existsSync(destFile)).to.be(true);
expect(fs.statSync(sourceFile).size).to.be(fs.statSync(destFile).size);
expect(oldStat.inode).to.not.be(fs.statSync(destFile).size);
done();
});
});
it('can backup', function (done) {
var tarStream = backups._createTarPackStream(gSourceFolder, gBackupConfig.key);
filesystem.upload(gBackupConfig, gBackupId_1, tarStream, function (error) {
expect(error).to.be(null);
it('can download file', function (done) {
var sourceFile = gTmpFolder + '/uploadtest/test.txt';
filesystem.download(gBackupConfig, sourceFile, function (error, stream) {
expect(error).to.be(null);
expect(stream).to.be.an('object');
done();
});
});
it('can download', function (done) {
filesystem.download(gBackupConfig, gBackupId_1, function (error, stream) {
expect(error).to.be(null);
backups._tarExtract(stream, gDestinationFolder, gBackupConfig.key || null, function (error) {
expect(error).to.be(null);
compareDirectories(path.join(gSourceFolder, 'data'), path.join(gDestinationFolder, 'data'), function (error) {
expect(error).to.equal(null);
compareDirectories(path.join(gSourceFolder, 'addon'), path.join(gDestinationFolder, 'addon'), function (error) {
expect(error).to.equal(null);
rimraf(gDestinationFolder, done);
});
});
});
});
});
it('can copy backup', function (done) {
// will be verified after removing the first and restoring from the copy
filesystem.copy(gBackupConfig, gBackupId_1, gBackupId_2, done);
});
it('can remove backup', function (done) {
// will be verified with next test trying to download the removed one
filesystem.remove(gBackupConfig, gBackupId_1, done);
});
it('cannot download deleted backup', function (done) {
filesystem.download(gBackupConfig, gBackupId_1, function (error, stream) {
expect(error).to.be.an('object');
expect(error.reason).to.equal(BackupsError.NOT_FOUND);
it('download errors for missing file', function (done) {
var sourceFile = gTmpFolder + '/uploadtest/missing';
filesystem.download(gBackupConfig, sourceFile, function (error) {
expect(error.reason).to.be(BackupsError.NOT_FOUND);
done();
});
});
it('can download backup copy', function (done) {
filesystem.download(gBackupConfig, gBackupId_2, function (error, stream) {
it('download dir copies contents of source dir', function (done) {
var sourceDir = path.join(__dirname, 'storage');
filesystem.downloadDir(gBackupConfig, sourceDir, gTmpFolder, function (error) {
expect(error).to.be(null);
backups._tarExtract(stream, gDestinationFolder, gBackupConfig.key || null, function (error) {
expect(error).to.be(null);
compareDirectories(path.join(gSourceFolder, 'data'), path.join(gDestinationFolder, 'data'), function (error) {
expect(error).to.equal(null);
compareDirectories(path.join(gSourceFolder, 'addon'), path.join(gDestinationFolder, 'addon'), function (error) {
expect(error).to.equal(null);
rimraf(gDestinationFolder, done);
});
});
});
expect(fs.statSync(path.join(gTmpFolder, 'data/empty')).size).to.be(0);
done();
});
});
it('can remove backup copy', function (done) {
filesystem.remove(gBackupConfig, gBackupId_2, done);
it('can copy', function (done) {
var sourceFile = gTmpFolder + '/uploadtest/test.txt'; // keep the test within save device
var destFile = gTmpFolder + '/uploadtest/test-hardlink.txt';
filesystem.copy(gBackupConfig, sourceFile, destFile, function (error) {
expect(error).to.be(null);
expect(fs.statSync(destFile).nlink).to.be(2); // created a hardlink
done();
});
});
it('can remove file', function (done) {
var sourceFile = gTmpFolder + '/uploadtest/test-hardlink.txt';
filesystem.remove(gBackupConfig, sourceFile, function (error) {
expect(error).to.be(null);
expect(fs.existsSync(sourceFile)).to.be(false);
done();
});
});
it('can remove empty dir', function (done) {
var sourceDir = gTmpFolder + '/emptydir';
fs.mkdirSync(sourceDir);
filesystem.remove(gBackupConfig, sourceDir, function (error) {
expect(error).to.be(null);
expect(fs.existsSync(sourceDir)).to.be(false);
done();
});
});
});
describe('noop', function () {
var gBackupConfig = {
provider: 'noop',
format: 'tgz'
};
it('upload works', function (done) {
noop.upload(gBackupConfig, 'file', { }, function (error) {
expect(error).to.be(null);
done();
});
});
it('can download file', function (done) {
noop.download(gBackupConfig, 'file', function (error) {
expect(error).to.be.an(Error);
done();
});
});
it('download dir copies contents of source dir', function (done) {
noop.downloadDir(gBackupConfig, 'sourceDir', 'destDir', function (error) {
expect(error).to.be.an(Error);
done();
});
});
it('can copy', function (done) {
noop.copy(gBackupConfig, 'sourceFile', 'destFile', function (error) {
expect(error).to.be(null);
done();
});
});
it('can remove file', function (done) {
noop.remove(gBackupConfig, 'sourceFile', function (error) {
expect(error).to.be(null);
done();
});
});
it('can remove empty dir', function (done) {
noop.remove(gBackupConfig, 'sourceDir', function (error) {
expect(error).to.be(null);
done();
});
});
});
describe('s3', function () {
this.timeout(10000);
var gBackupId_1 = 'someprefix/one';
var gBackupId_2 = 'someprefix/two';
var gTmpFolder;
var gSourceFolder;
var gDestinationFolder;
var gS3Folder;
var gBackupConfig = {
provider: 's3',
key: 'key',
@@ -242,113 +205,76 @@ describe('Storage', function () {
format: 'tgz'
};
before(function (done) {
before(function () {
MockS3.config.basePath = path.join(os.tmpdir(), 's3-backup-test-buckets/');
rimraf.sync(MockS3.config.basePath);
gS3Folder = path.join(MockS3.config.basePath, gBackupConfig.bucket);
s3._mockInject(MockS3);
setup(function (error) {
expect(error).to.be(null);
gTmpFolder = fs.mkdtempSync(path.join(os.tmpdir(), 's3-backup-test_'));
gSourceFolder = path.join(__dirname, 'storage');
gDestinationFolder = path.join(gTmpFolder, 'destination/');
settings.setBackupConfig(gBackupConfig, function (error) {
expect(error).to.be(null);
done();
});
});
});
after(function (done) {
after(function () {
s3._mockRestore();
rimraf.sync(MockS3.config.basePath);
});
cleanup(function (error) {
it('can upload', function (done) {
var sourceFile = path.join(__dirname, 'storage/data/test.txt');
var sourceStream = fs.createReadStream(sourceFile);
var destKey = 'uploadtest/test.txt';
s3.upload(gBackupConfig, destKey, sourceStream, function (error) {
expect(error).to.be(null);
rimraf.sync(gTmpFolder);
expect(fs.existsSync(path.join(gS3Folder, destKey))).to.be(true);
expect(fs.statSync(path.join(gS3Folder, destKey)).size).to.be(fs.statSync(sourceFile).size);
done();
});
});
it('can backup', function (done) {
var tarStream = backups._createTarPackStream(gSourceFolder, gBackupConfig.key);
s3.upload(gBackupConfig, gBackupId_1, tarStream, function (error) {
it('can download file', function (done) {
var sourceKey = 'uploadtest/test.txt';
s3.download(gBackupConfig, sourceKey, function (error, stream) {
expect(error).to.be(null);
expect(stream).to.be.an('object');
done();
});
});
it('can download', function (done) {
s3.download(gBackupConfig, gBackupId_1, function (error, stream) {
it('download dir copies contents of source dir', function (done) {
var sourceFile = path.join(__dirname, 'storage/data/test.txt');
var sourceKey = '';
var destDir = path.join(os.tmpdir(), 's3-destdir');
s3.downloadDir(gBackupConfig, sourceKey, destDir, function (error) {
expect(error).to.be(null);
backups._tarExtract(stream, gDestinationFolder, gBackupConfig.key || null, function (error) {
expect(error).to.be(null);
compareDirectories(path.join(gSourceFolder, 'data'), path.join(gDestinationFolder, 'data'), function (error) {
expect(error).to.equal(null);
compareDirectories(path.join(gSourceFolder, 'addon'), path.join(gDestinationFolder, 'addon'), function (error) {
expect(error).to.equal(null);
rimraf(gDestinationFolder, done);
});
});
});
expect(fs.statSync(path.join(destDir, 'uploadtest/test.txt')).size).to.be(fs.statSync(sourceFile).size);
done();
});
});
it('can copy backup', function (done) {
// will be verified after removing the first and restoring from the copy
progress.set(progress.BACKUP, 10, 'Testing');
it('can copy', function (done) {
var sourceKey = 'uploadtest';
s3.copy(gBackupConfig, gBackupId_1, gBackupId_2, done);
});
it('can remove backup', function (done) {
// will be verified with next test trying to download the removed one
s3.remove(gBackupConfig, gBackupId_1, done);
});
it('cannot download deleted backup', function (done) {
s3.download(gBackupConfig, gBackupId_1, function (error, stream) {
s3.copy(gBackupConfig, sourceKey, 'uploadtest-copy', function (error) {
var sourceFile = path.join(__dirname, 'storage/data/test.txt');
expect(error).to.be(null);
stream.on('error', function (error) {
expect(error).to.be.an('object');
expect(error.reason).to.equal(BackupsError.NOT_FOUND);
done();
});
expect(fs.statSync(path.join(gS3Folder, 'uploadtest-copy/test.txt')).size).to.be(fs.statSync(sourceFile).size);
done();
});
});
it('can download backup copy', function (done) {
s3.download(gBackupConfig, gBackupId_2, function (error, stream) {
it('can remove file', function (done) {
s3.remove(gBackupConfig, 'uploadtest-copy/test.txt', function (error) {
expect(error).to.be(null);
backups._tarExtract(stream, gDestinationFolder, gBackupConfig.key || null, function (error) {
expect(error).to.be(null);
compareDirectories(path.join(gSourceFolder, 'data'), path.join(gDestinationFolder, 'data'), function (error) {
expect(error).to.equal(null);
compareDirectories(path.join(gSourceFolder, 'addon'), path.join(gDestinationFolder, 'addon'), function (error) {
expect(error).to.equal(null);
rimraf(gDestinationFolder, done);
});
});
});
expect(fs.existsSync(path.join(gS3Folder, 'uploadtest-copy/test.txt'))).to.be(false);
done();
});
});
it('can remove backup copy', function (done) {
s3.remove(gBackupConfig, gBackupId_2, done);
it('can remove non-existent dir', function (done) {
noop.remove(gBackupConfig, 'blah', function (error) {
expect(error).to.be(null);
done();
});
});
});
});
View File