-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.js
209 lines (178 loc) · 8.64 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
'use strict';
const _ = require('lodash');
const async = require('async');
const child_process = require('child_process');
const fs = require('fs');
const moment = require('moment');
const mysql = require('mysql');
const path = require('path');
const uuid = require('uuid');
class DbTestUtil {
constructor(options) {
this.options = _.defaultsDeep({}, options || {}, {
mysql: 'mysql',
databaseMustEndWith: '_test',
hostBlacklist: [],
charset: "utf8mb4",
collate: "utf8mb4_unicode_520_ci",
});
}
createTestDb(connectionConfig, sqlFiles, callback) {
const readConfig = (filepath) => {
let json;
try {
json = fs.readFileSync(filepath).toString();
} catch (err) {
json = '{}'; // ignore file not found
}
return JSON.parse(json); // don't catch this error -- let the user see it.
};
connectionConfig = _.defaultsDeep(
connectionConfig, // supplied config, modifies connectionConfig
readConfig(path.join(process.env.HOME || `${path.sep}/root`, '.dbtestutil.conf')), // /home/jdoe/.dbtestutil.conf
readConfig(path.join(`${path.sep}usr`, 'local', 'etc', 'dbtestutil.conf')), // /usr/local/etc/dbtestutil.conf
readConfig(path.join(`${path.sep}etc`, 'dbtestutil.conf')), // /etc/dbtestutil.conf
{ // defaults
user: 'root',
password: '',
host: 'localhost',
port: 3306,
database: '',
multipleStatements: true,
selfDestruct: 'PT6H',
charset: this.options.collate, // mysqljs accpets either SQL-level "charset" or "collation" here. Pass collate as it's more specific.
}
);
async.waterfall([
/*
* Preflight Checks
*/
(callback) => {
// The test suite MUST NOT run if there is no suffix (to avoid clobbering active databases)
if (!connectionConfig.database.endsWith(this.options.databaseMustEndWith)) {
const missingSuffixError = new Error('database name missing test suffix');
missingSuffixError.name = 'DBTESTUTIL_DATABASE_MISSING_SUFFIX';
missingSuffixError.database = connectionConfig.database;
missingSuffixError.databaseMustEndWith = this.options.databaseMustEndWith;
return callback(missingSuffixError);
}
// The test suite should check to see if it's running against the prod server and refuse to run if it is for safety
if (_.includes(this.options.hostBlacklist, connectionConfig.host)) {
const hostBlacklistedError = new Error('host must not appear in hostBlacklist');
hostBlacklistedError.name = 'DBTESTUTIL_HOST_BLACKLISTED';
hostBlacklistedError.host = connectionConfig.host;
hostBlacklistedError.hostBlacklist = this.options.hostBlacklist;
return callback(hostBlacklistedError);
}
// The test suite should not allow itself to run in a production environment.
if (process.env.NODE_ENV === 'production') {
const productionEnvironmentError = new Error('this seems to be a production environment');
productionEnvironmentError.name = 'DBTESTUTIL_PRODUCTION_ENVIRONMENT';
productionEnvironmentError.node_env = process.env.NODE_ENV;
return callback(productionEnvironmentError);
}
callback();
},
/*
* Create the database
*/
(callback) => {
const conn = mysql.createConnection(_.omit(connectionConfig, ['database']));
conn.query('CREATE DATABASE ?? CHARACTER SET ?? COLLATE ??;', [ connectionConfig.database, this.options.charset, this.options.collate ], (err, result) => {
conn.end();
if (err) {
const dbCreateError = new Error('could not create database');
dbCreateError.name = 'DBTESTUTIL_DB_CREATE';
dbCreateError.inner = err;
return callback(dbCreateError);
}
callback();
});
},
/*
* Set Self Destruct -- do this *before* schema load so that if there is a load issue, the database still gets cleaned up.
*/
(callback) => {
if (!_.isString(connectionConfig.selfDestruct)) { // skip
return callback();
}
const conn = mysql.createConnection(connectionConfig);
async.eachSeries([{
sql: 'CREATE EVENT ?? ON SCHEDULE AT ? DO DROP DATABASE ??',
values: [
`${connectionConfig.database}_self_destruct`,
moment().add(moment.duration(connectionConfig.selfDestruct)).toDate(),
connectionConfig.database,
],
}, {
sql: 'SET GLOBAL event_scheduler = ON',
}], (query, callback) => {
conn.query(query, (err) => {
if (err) {
const dbEventSetupError = new Error('could not create event to self destruct database');
dbEventSetupError.name = 'DBTESTUTIL_DB_EVENT';
dbEventSetupError.database = connectionConfig.database;
dbEventSetupError.inner = err;
return callback(dbEventSetupError);
}
callback();
});
}, (err) => {
conn.end();
callback(err);
});
},
/*
* Load SQL File(s)
*/
(callback) => {
async.eachSeries(sqlFiles, (sqlFile, callback) => {
const args = [
`--default-character-set=${this.options.charset}`,
'--user', connectionConfig.user,
];
if (connectionConfig.socketPath) { // when present, take priority over host/port config T3511
args.push('--socket');
args.push(connectionConfig.socketPath);
} else {
args.push('--host');
args.push(connectionConfig.host);
args.push('--port');
args.push(connectionConfig.port);
}
if (connectionConfig.password) {
args.push(`-p${connectionConfig.password}`);
}
args.push(connectionConfig.database);
const proc = child_process.spawn(this.options.mysql, args);
let stdout = '';
proc.stdout.on('data', (data) => stdout += data);
let stderr = '';
proc.stderr.on('data', (data) => stdout += data);
proc.on('close', (code) => {
if (code !== 0) {
const mysqlCommandError = new Error('problem executing mysql command');
mysqlCommandError.name = 'DBTESTUTIL_MYSQL_CMD';
mysqlCommandError.code = code;
mysqlCommandError.stdout = stdout;
mysqlCommandError.stderr = stderr;
mysqlCommandError.cmd = this.options.mysql;
mysqlCommandError.args = args;
return callback(mysqlCommandError);
}
callback();
});
fs.createReadStream(sqlFile).pipe(proc.stdin);
}, callback);
},
], callback);
}
static makeDatabaseName(stem, suffix, separator) {
return [
stem || 'dbtestutil',
uuid.v1().split('-')[0],
suffix || 'test'
].join(separator || '_');
}
}
module.exports = DbTestUtil;