🎨 Support LTS imports (#8498)

refs #8141

- update importer for LTS fields
- optimise for LTS export fixtures
- add image/language test for LTS import
- ensure post image is mapped to feature_image
- create mobiledoc values from markdown and html
- if mobiledoc is null, use markdown or html to create a mobiledoc markdown card
- update import mapping to use locale
- defaultLang in settings now maps to default_locale
- language for post and user models now maps to locale
- posts are not always loaded in correct same order so we select the posts we want to validate
- ensure if mobiledoc field is not in export we can still import from markdown
- map last_login to last_seen
- for users the importer maps last_login to last_seen
- add warning for legacyActiveTheme
- for export with old activeTheme key provide a warning that theme is not installed
- add importer test for LTS user long email
- add a test for LTS export where email address could be longer than alpha
- fix for importer date tests on mysql
- use valueOf in moment to compare times stored in different formats
- ignore warnings for not found settings in import
- use a flag to ignore NotFound Entries for settings during import
This commit is contained in:
David Wolfe 2017-06-04 10:53:00 +01:00 committed by Katharina Irrgang
parent 9023ff0b68
commit b081ae34b5
31 changed files with 3644 additions and 41 deletions

View File

@ -16,7 +16,15 @@ class Base {
this.errorConfig = {
allowDuplicates: true,
returnDuplicates: true
returnDuplicates: true,
showNotFoundWarning: true
};
this.legacyKeys = {};
this.legacyMapper = function legacyMapper(item) {
return _.mapKeys(item, function matchLegacyKey(value, key) {
return self.legacyKeys[key] || key;
});
};
this.dataKeyToImport = options.dataKeyToImport;
@ -79,12 +87,14 @@ class Base {
}));
}
} else if (err instanceof errors.NotFoundError) {
problems.push({
message: 'Entry was not imported and ignored. Could not find entry.',
help: self.modelName,
context: JSON.stringify(obj),
err: err
});
if (self.showNotFoundWarning) {
problems.push({
message: 'Entry was not imported and ignored. Could not find entry.',
help: self.modelName,
context: JSON.stringify(obj),
err: err
});
}
} else {
if (!errors.utils.isIgnitionError(err)) {
err = new errors.DataImportError({

View File

@ -13,6 +13,10 @@ class PostsImporter extends BaseImporter {
dataKeyToImport: 'posts',
requiredData: ['tags', 'posts_tags']
}));
this.legacyKeys = {
image: 'feature_image'
};
}
sanitizeAttributes() {
@ -99,10 +103,42 @@ class PostsImporter extends BaseImporter {
beforeImport() {
debug('beforeImport');
let mobileDocContent, self = this;
this.sanitizeAttributes();
this.addTagsToPosts();
// Remove legacy field language
this.dataToImport = _.filter(this.dataToImport, function (data) {
return _.omit(data, 'language');
});
this.dataToImport = this.dataToImport.map(self.legacyMapper);
// For legacy imports/custom imports with only html we can parse the markdown or html into a mobile doc card
// For now we can hardcode the version
_.each(this.dataToImport, function (model) {
if (!model.mobiledoc) {
if (model.markdown && model.markdown.length > 0) {
mobileDocContent = model.markdown;
} else if (model.html && model.html.length > 0) {
mobileDocContent = model.html;
} else {
// Set mobileDocContent to null else it will affect empty posts
mobileDocContent = null;
}
if (mobileDocContent) {
model.mobiledoc = JSON.stringify({
version: '0.3.1',
markups: [],
atoms: [],
cards: [['card-markdown',{cardName: 'card-markdown',markdown: mobileDocContent}]],
sections:[[10,0]]
});
}
}
});
// NOTE: do after, because model properties are deleted e.g. post.id
return super.beforeImport();
}

View File

@ -14,9 +14,17 @@ class SettingsImporter extends BaseImporter {
requiredData: []
}));
this.legacyKeys = {
activePlugins: 'active_apps',
installedPlugins: 'installed_apps'
this.errorConfig = {
allowDuplicates: true,
returnDuplicates: true,
showNotFoundWarning: false
};
// Map legacy keys
this.legacySettingsKeyValues = {
isPrivate: 'is_private',
activeTimezone: 'active_timezone',
cover: 'cover_image'
};
}
@ -27,14 +35,25 @@ class SettingsImporter extends BaseImporter {
beforeImport() {
debug('beforeImport');
let self = this;
let self = this,
ltsActiveTheme = _.find(this.dataToImport, {key: 'activeTheme'});
// If there is an lts we want to warn user that theme is not imported
if (ltsActiveTheme) {
self.problems.push({
message: 'Theme not imported, please upload in Settings - Design',
help: self.modelName,
context: JSON.stringify(ltsActiveTheme)
});
}
// Remove core and theme data types
this.dataToImport = _.filter(this.dataToImport, function (data) {
return ['core', 'theme'].indexOf(data.type) === -1;
});
_.each(this.dataToImport, function (obj) {
obj.key = self.legacyKeys[obj.key] || obj.key;
obj.key = self.legacySettingsKeyValues[obj.key] || obj.key;
});
return super.beforeImport();

View File

@ -13,6 +13,11 @@ class TagsImporter extends BaseImporter {
dataKeyToImport: 'tags',
requiredData: []
}));
// Map legacy keys
this.legacyKeys = {
image: 'feature_image'
};
}
beforeImport() {
@ -32,6 +37,8 @@ class TagsImporter extends BaseImporter {
let self = this, ops = [];
this.dataToImport = this.dataToImport.map(self.legacyMapper);
_.each(this.dataToImport, function (obj) {
ops.push(models[self.modelName].findOne({name: obj.name}, options).then(function (tag) {
if (tag) {

View File

@ -12,6 +12,13 @@ class UsersImporter extends BaseImporter {
dataKeyToImport: 'users',
requiredData: ['roles', 'roles_users']
}));
// Map legacy keys
this.legacyKeys = {
image: 'profile_image',
cover: 'cover_image',
last_login: 'last_seen'
};
}
/**
@ -25,6 +32,13 @@ class UsersImporter extends BaseImporter {
let self = this, role;
// Remove legacy field language
this.dataToImport = _.filter(this.dataToImport, function (data) {
return _.omit(data, 'language');
});
this.dataToImport = this.dataToImport.map(self.legacyMapper);
_.each(this.dataToImport, function (model) {
model.password = globalUtils.uid(50);
model.status = 'locked';

View File

@ -39,7 +39,7 @@ describe('Import', function () {
it('import results have data and problems', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-003').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-003', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function (importResult) {
@ -54,14 +54,13 @@ describe('Import', function () {
it('removes duplicate posts', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-003').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-003',{lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function (importResult) {
should.exist(importResult.data.posts);
importResult.data.posts.length.should.equal(1);
importResult.problems.length.should.eql(8);
importResult.problems.length.should.eql(2);
done();
}).catch(done);
@ -70,7 +69,7 @@ describe('Import', function () {
it('removes duplicate tags and updates associations', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-003-duplicate-tags').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-003-duplicate-tags', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function (importResult) {
@ -87,7 +86,9 @@ describe('Import', function () {
return postTag.tag_id !== 2;
});
importResult.problems.length.should.equal(9);
importResult.problems.length.should.equal(3);
importResult.problems[2].message.should.equal('Theme not imported, please upload in Settings - Design');
done();
}).catch(done);
@ -100,7 +101,7 @@ describe('Import', function () {
it('imports data from 000', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-000').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-000', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function () {
@ -146,7 +147,7 @@ describe('Import', function () {
it('safely imports data, from 001', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-001').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-001', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function () {
@ -203,7 +204,7 @@ describe('Import', function () {
it('doesn\'t import invalid settings data from 001', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-001-invalid-setting').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-001-invalid-setting', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function () {
@ -244,7 +245,7 @@ describe('Import', function () {
it('safely imports data from 002', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-002').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-002', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function () {
@ -305,7 +306,7 @@ describe('Import', function () {
it('safely imports data from 003 (single user)', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-003').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-003', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function () {
@ -344,7 +345,7 @@ describe('Import', function () {
it('handles validation errors nicely', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-003-badValidation').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-003-badValidation', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function () {
@ -368,7 +369,7 @@ describe('Import', function () {
it('handles database errors nicely: duplicated tag slugs', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-003-dbErrors').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-003-dbErrors', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function (importedData) {
@ -386,7 +387,7 @@ describe('Import', function () {
it('does import posts with an invalid author', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-003-mu-unknownAuthor').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-003-mu-unknownAuthor', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function (importedData) {
@ -436,7 +437,7 @@ describe('Import', function () {
it('doesn\'t import invalid tags data from 003', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-003-nullTags').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-003-nullTags', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function () {
@ -454,7 +455,7 @@ describe('Import', function () {
it('doesn\'t import invalid posts data from 003', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-003-nullPosts').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-003-nullPosts', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function () {
@ -475,7 +476,7 @@ describe('Import', function () {
it('correctly sanitizes incorrect UUIDs', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-003-wrongUUID').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-003-wrongUUID', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function () {
@ -499,7 +500,7 @@ describe('Import', function () {
it('ensure post tag order is correct', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-004').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-004', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function () {
@ -547,7 +548,7 @@ describe('Import', function () {
it('doesn\'t import a title which is too long', function (done) {
var exportData;
testUtils.fixtures.loadExportFixture('export-001').then(function (exported) {
testUtils.fixtures.loadExportFixture('export-001', {lts:true}).then(function (exported) {
exportData = exported;
// change title to 1001 characters
@ -596,7 +597,7 @@ describe('Import (new test structure)', function () {
before(function doImport(done) {
testUtils.initFixtures('roles', 'owner', 'settings').then(function () {
return testUtils.fixtures.loadExportFixture('export-003-mu');
return testUtils.fixtures.loadExportFixture('export-003-mu', {lts:true});
}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
@ -818,7 +819,7 @@ describe('Import (new test structure)', function () {
before(function doImport(done) {
testUtils.initFixtures('roles', 'owner', 'settings').then(function () {
return testUtils.fixtures.loadExportFixture('export-003-mu-noOwner');
return testUtils.fixtures.loadExportFixture('export-003-mu-noOwner', {lts:true});
}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
@ -1041,7 +1042,7 @@ describe('Import (new test structure)', function () {
before(function doImport(done) {
// initialise the blog with some data
testUtils.initFixtures('users:roles', 'posts', 'settings').then(function () {
return testUtils.fixtures.loadExportFixture('export-003-mu');
return testUtils.fixtures.loadExportFixture('export-003-mu', {lts:true});
}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
@ -1270,7 +1271,7 @@ describe('Import (new test structure)', function () {
before(function doImport(done) {
// initialise the blog with some data
testUtils.initFixtures('users:roles', 'posts', 'settings').then(function () {
return testUtils.fixtures.loadExportFixture('export-003-mu-multipleOwner');
return testUtils.fixtures.loadExportFixture('export-003-mu-multipleOwner', {lts: true});
}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
@ -1334,4 +1335,221 @@ describe('Import (new test structure)', function () {
}).catch(done);
});
});
describe('lts: legacy fields', function () {
var exportData;
before(function doImport(done) {
// initialise the blog with some data
testUtils.initFixtures('roles', 'owner', 'settings').then(function () {
return testUtils.fixtures.loadExportFixture('export-lts-legacy-fields', {lts: true});
}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function () {
done();
}).catch(done);
});
after(testUtils.teardown);
it('ensure data is still imported and mapped correctly', function (done) {
var fetchImported = Promise.join(
knex('users').select(),
knex('posts').select(),
knex('tags').select(),
knex('settings').select()
);
fetchImported
.then(function (importedData) {
should.exist(importedData);
importedData.length.should.equal(4);
var users = importedData[0],
posts = importedData[1],
tags = importedData[2],
settings = importedData[3],
firstPost = _.find(posts, {slug: exportData.data.posts[0].slug});
// Check length of of posts, tags and users
posts.length.should.equal(exportData.data.posts.length);
tags.length.should.equal(exportData.data.tags.length);
// Users include original user + joe bloggs' brother
users.length.should.equal(exportData.data.users.length + 1);
// Check feature image is correctly mapped for a post
firstPost.feature_image.should.eql('/content/images/2017/05/post-image.jpg');
// Check logo and cover images are correctly mapped for a user
users[1].cover_image.should.eql(exportData.data.users[0].cover);
users[1].profile_image.should.eql(exportData.data.users[0].image);
// Check feature image is correctly mapped for a tag
tags[0].feature_image.should.eql(exportData.data.tags[0].image);
// Check logo image is correctly mapped for a blog
settings[6].key.should.eql('logo');
settings[6].value.should.eql('/content/images/2017/05/bloglogo.jpeg');
// Check cover image is correctly mapped for a blog
settings[7].key.should.eql('cover_image');
settings[7].value.should.eql('/content/images/2017/05/blogcover.jpeg');
// Check default settings locale is not overwritten by defaultLang
settings[9].key.should.eql('default_locale');
settings[9].value.should.eql('en');
// Check post language is null
should(firstPost.locale).equal(null);
// Check user language is null
should(users[1].locale).equal(null);
// Check mobiledoc is populated from markdown
JSON.parse(firstPost.mobiledoc).cards[0][1].markdown.should.eql(exportData.data.posts[0].markdown);
done();
}).catch(done);
});
});
describe('lts: style import with missing markdown or html values', function () {
var exportData;
before(function doImport(done) {
// initialise the blog with some data
testUtils.initFixtures('roles', 'owner', 'settings').then(function () {
return testUtils.fixtures.loadExportFixture('export-lts-style-bad-markdown-html',
{lts: true}
);
}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function () {
done();
}).catch(done);
});
after(testUtils.teardown);
it('ensure images are mapped correctly and language is null', function (done) {
var fetchImported = Promise.join(
knex('users').select(),
knex('posts').select(),
knex('tags').select(),
knex('settings').select()
);
fetchImported.then(function (importedData) {
should.exist(importedData);
importedData.length.should.equal(4);
var users = importedData[0],
posts = importedData[1],
tags = importedData[2],
settings = importedData[3],
firstPost = _.find(posts, {slug: exportData.data.posts[0].slug}),
secondPost = _.find(posts, {slug: exportData.data.posts[1].slug}),
thirdPost = _.find(posts, {slug: exportData.data.posts[2].slug}),
fourthPost = _.find(posts, {slug: exportData.data.posts[3].slug});
// Check length of of posts, tags and users
posts.length.should.equal(exportData.data.posts.length);
tags.length.should.equal(exportData.data.tags.length);
// Users include original user + joe bloggs' brother
users.length.should.equal(exportData.data.users.length + 1);
// Check feature image is correctly mapped for a post
should(firstPost.feature_image).equal(null);
// Check logo and cover images are correctly mapped for a user
users[1].cover_image.should.eql(exportData.data.users[0].cover);
users[1].profile_image.should.eql(exportData.data.users[0].image);
// Check feature image is correctly mapped for a tag
tags[0].feature_image.should.eql(exportData.data.tags[0].image);
// Check logo image is correctly mapped for a blog
settings[6].key.should.eql('logo');
settings[6].value.should.eql('/content/images/2017/05/bloglogo.jpeg');
// Check cover image is correctly mapped for a blog
settings[7].key.should.eql('cover_image');
settings[7].value.should.eql('/content/images/2017/05/blogcover.jpeg');
// Check default settings locale is not overwritten by defaultLang
settings[9].key.should.eql('default_locale');
settings[9].value.should.eql('en');
// Check post language is set to null
should(firstPost.locale).equal(null);
// Check user language is set to null
should(users[1].locale).equal(null);
// Check last_seen is mapped from last_login for user
assert.equal(
moment(users[1].last_seen).valueOf(),
moment(exportData.data.users[0].last_login).valueOf()
);
// Check mobiledoc is populated from from html when mobiledoc is null & markdown is empty string
JSON.parse(firstPost.mobiledoc).cards[0][1].markdown.should.eql(exportData.data.posts[0].html);
// Check mobiledoc is populated from from html when mobiledoc is null & markdown is null
JSON.parse(secondPost.mobiledoc).cards[0][1].markdown.should.eql(exportData.data.posts[1].html);
// Check mobiledoc is null when markdown and mobiledoc are null and html is empty string
should(thirdPost.mobiledoc).equal(null);
// Check mobiledoc is null when markdown, mobiledoc are html are null
should(fourthPost.mobiledoc).equal(null);
done();
}).catch(done);
});
it('ensure post without mobiledoc key uses markdown', function (done) {
var fetchImported = Promise.resolve(knex('posts').select());
fetchImported.then(function (importedData) {
should.exist(importedData);
importedData.length.should.equal(5);
var posts = importedData,
fifthPost = _.find(posts, {slug: exportData.data.posts[4].slug});
// Check mobiledoc is populated from from html when mobiledoc is null & markdown is empty string
JSON.parse(fifthPost.mobiledoc).cards[0][1].markdown.should.eql(exportData.data.posts[4].markdown);
done();
}).catch(done);
});
});
describe('lts: style import for user with a very long email address', function () {
var exportData;
before(function doImport(done) {
// initialise the blog with some data
testUtils.initFixtures('roles', 'owner', 'settings').then(function () {
return testUtils.fixtures.loadExportFixture('export-lts-style-user-long-email',
{lts: true}
);
}).then(function (exported) {
exportData = exported;
done();
}).catch(done);
});
after(testUtils.teardown);
it('provides error message and does not import where lts email address is longer that 1.0.0 constraint', function (done) {
testUtils.fixtures.loadExportFixture('export-lts-style-user-long-email', {lts:true}).then(function (exported) {
exportData = exported;
return dataImporter.doImport(exportData);
}).then(function () {
(1).should.eql(0, 'Data import should not resolve promise.');
}).catch(function (error) {
error[0].message.should.eql('Value in [users.email] exceeds maximum length of 191 characters.');
error[0].errorType.should.eql('ValidationError');
Promise.resolve(knex('users').select()).then(function (users) {
should.exist(users);
users.length.should.equal(1, 'Did not get data successfully');
users[0].email.should.not.equal(exportData.data.users[0].email);
done();
});
});
});
});
});

View File

@ -313,7 +313,7 @@ describe('Importer', function () {
it('correctly handles a valid db api wrapper', function (done) {
var file = [{
path: testUtils.fixtures.getExportFixturePath('export-003-api-wrapper'),
path: testUtils.fixtures.getExportFixturePath('export-003-api-wrapper', {lts: true}),
name: 'export-003-api-wrapper.json'
}];
JSONHandler.loadFile(file).then(function (result) {
@ -325,7 +325,7 @@ describe('Importer', function () {
it('correctly errors when given a bad db api wrapper', function (done) {
var file = [{
path: testUtils.fixtures.getExportFixturePath('export-003-api-wrapper-bad'),
path: testUtils.fixtures.getExportFixturePath('export-003-api-wrapper-bad', {lts: true}),
name: 'export-003-api-wrapper-bad.json'
}];

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
{
"meta": {
"exported_on": 1496145843289,
"version": "009"
},
"data": {
"users": [
{
"id": 1,
"uuid": "1e2a7354-f580-4deb-9801-ca286628125a",
"name": "Joe Blogg's long email Brother",
"slug": "joe-bloggs-long-email-brother",
"password": "$2a$10$.pZeeBE0gHXd0PTnbT/ph.GEKgd0Wd3q2pWna3ynTGBkPKnGIKABC",
"email": "thisisareallylongemailaddressIamhappytobeusingacharactercounterbutIhavealongwaytogoyetImeanserioulsywhohasemailaddressthislongthereisnowaythiswillpassvalidationsonghost100andisarealedgecase@example.com",
"image": "/content/images/2017/05/authorlogo.jpeg",
"cover": "/content/images/2017/05/authorcover.jpeg",
"bio": "I'm Joe's brother, the good looking one!",
"website": "http://joebloggslongemailbrother.com",
"location": null,
"facebook": null,
"twitter": null,
"accessibility": null,
"status": "active",
"language": "en_US",
"visibility": "public",
"meta_title": null,
"meta_description": null,
"tour": null,
"last_login": "2017-05-30T10:39:32.000Z",
"created_at": "2016-10-28T13:43:36.000Z",
"created_by": 1,
"updated_at": "2017-05-30T12:02:35.000Z",
"updated_by": 1
}
]
}
}

View File

@ -313,12 +313,15 @@ fixtures = {
return path.resolve(__dirname + '/fixtures/import/' + filename);
},
getExportFixturePath: function (filename) {
return path.resolve(__dirname + '/fixtures/export/' + filename + '.json');
getExportFixturePath: function (filename, options) {
options = options || {lts: false};
var relativePath = options.lts ? '/fixtures/export/lts/' : '/fixtures/export/';
return path.resolve(__dirname + relativePath + filename + '.json');
},
loadExportFixture: function loadExportFixture(filename) {
var filePath = this.getExportFixturePath(filename),
loadExportFixture: function loadExportFixture(filename, options) {
options = options || {lts: false};
var filePath = this.getExportFixturePath(filename, options),
readFile = Promise.promisify(fs.readFile);
return readFile(filePath).then(function (fileContents) {