diff --git a/.gitignore b/.gitignore index 710ec373..c7687df8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,10 @@ npm-debug.log .DS_Store config/development.* config/production.* +config/test.* workers/reports/config/development.* workers/reports/config/production.* +workers/reports/config/test.* dump.rdb # generate POT file every time you want to update your PO file diff --git a/Gruntfile.js b/Gruntfile.js index 6ac983cc..daf89982 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -9,7 +9,7 @@ module.exports = function (grunt) { }, nodeunit: { - all: ['test/**/*-test.js'] + all: ['test/nodeunit/**/*-test.js'] }, jsxgettext: { diff --git a/app.js b/app.js index f8bad1bf..1143df71 100644 --- a/app.js +++ b/app.js @@ -183,6 +183,11 @@ app.use((req, res, next) => { res.locals.customStyles = config.customstyles || []; res.locals.customScripts = config.customscripts || []; + let bodyClasses = []; + app.get('env') === 'test' && bodyClasses.push('page--' + (req.path.substring(1).replace(/\//g, '--') || 'home')); + req.user && bodyClasses.push('logged-in user-' + req.user.username); + res.locals.bodyClass = bodyClasses.join(' '); + settingsModel.list(['ua_code', 'shoutout'], (err, configItems) => { if (err) { return next(err); diff --git a/config/default.toml b/config/default.toml index 64d45fbe..af72e6dd 100644 --- a/config/default.toml +++ b/config/default.toml @@ -110,16 +110,6 @@ host="0.0.0.0" # VERP hostname is in the same domain as the From address. # disablesenderheader=true -[testserver] -# Starts a vanity server that redirects all mail to /dev/null -# Mostly needed for local development -enabled=false -port=5587 -host="0.0.0.0" -username="testuser" -password="testpass" -logger=false - [ldap] # enable to use ldap user backend enabled=false @@ -177,3 +167,17 @@ templates=[["demo", "Demo Template"]] # The bottom line is that if people who are creating report templates or have write access to the DB cannot be trusted, # then it's safer to switch off the reporting functionality below. enabled=false + +[testserver] +# Starts a vanity server that redirects all mail to /dev/null +# Mostly needed for local development +enabled=false +port=5587 +mailboxserverport=3001 +host="0.0.0.0" +username="testuser" +password="testpass" +logger=false + +[seleniumwebdriver] +browser="phantomjs" diff --git a/lib/dbcheck.js b/lib/dbcheck.js index 001723a1..93264800 100644 --- a/lib/dbcheck.js +++ b/lib/dbcheck.js @@ -107,13 +107,14 @@ function getSql(path, data, callback) { if (err) { return callback(err); } - let renderer = Handlebars.compile(source); - return callback(null, renderer(data || {})); + const rendered = data ? Handlebars.compile(source)(data) : source; + return callback(null, rendered); }); } function runInitial(callback) { - let fname = process.env.DB_FROM_START ? 'base.sql' : 'mailtrain.sql'; + let dump = process.env.NODE_ENV === 'test' ? 'mailtrain-test.sql' : 'mailtrain.sql'; + let fname = process.env.DB_FROM_START ? 'base.sql' : dump; let path = pathlib.join(__dirname, '..', 'setup', 'sql', fname); log.info('sql', 'Loading tables from %s', fname); applyUpdate({ diff --git a/package.json b/package.json index 7d82f7cd..29c98088 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,17 @@ "test": "grunt", "start": "node index.js", "sqlinit": "node setup/sql/init.js", - "sqldump": "node setup/sql/dump.js | sed -e '/^\\/\\*.*\\*\\/;$/d' -e 's/.[0-9]\\{4\\}-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]./NOW()/g' > setup/sql/mailtrain.sql", + "sqldump": "node setup/sql/dump.js | sed -e '/^\\/\\*.*\\*\\/;$/d' -e 's/.[0-9]\\{4\\}-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]./NOW()/g' > setup/sql/mailtrain${DUMP_NAME_SUFFIX}.sql", "sqldrop": "node setup/sql/drop.js", "sqlgen": "npm run sqldrop && DB_FROM_START=Y npm run sqlinit && npm run sqldump", "langs:hbs": "jsxgettext -L handlebars -k translate -o langs/hbs.pot views/layout.hbs views/index.hbs", "langs:js": "jsxgettext -o languages/js.pot routes/index.js", - "langs": "npm run langs:hbs && npm run langs:js" + "langs": "npm run langs:hbs && npm run langs:js", + "sqldumptest": "NODE_ENV=test DUMP_NAME_SUFFIX=-test npm run sqldump", + "sqlresettest": "NODE_ENV=test npm run sqldrop && NODE_ENV=test npm run sqlinit", + "starttest": "NODE_ENV=test node index.js", + "_e2e": "NODE_ENV=test mocha test/e2e/index.js", + "e2e": "npm run sqlresettest && npm run _e2e" }, "repository": { "type": "git", @@ -26,12 +31,18 @@ "node": ">=5.0.0" }, "devDependencies": { + "babel-eslint": "^7.2.3", + "chai": "^3.5.0", "eslint-config-nodemailer": "^1.0.0", "grunt": "^1.0.1", "grunt-cli": "^1.2.0", "grunt-contrib-nodeunit": "^1.0.0", "grunt-eslint": "^19.0.0", - "jsxgettext-andris": "^0.9.0-patch.1" + "jsxgettext-andris": "^0.9.0-patch.1", + "mailparser": "^2.0.5", + "mocha": "^3.3.0", + "phantomjs": "^2.1.7", + "selenium-webdriver": "^3.4.0" }, "optionalDependencies": { "posix": "^4.1.1" diff --git a/services/test-server.js b/services/test-server.js index 025a16df..1f4777b8 100644 --- a/services/test-server.js +++ b/services/test-server.js @@ -4,12 +4,37 @@ let log = require('npmlog'); let config = require('config'); let crypto = require('crypto'); let humanize = require('humanize'); +let http = require('http'); let SMTPServer = require('smtp-server').SMTPServer; +let simpleParser = require('mailparser').simpleParser; let totalMessages = 0; let received = 0; +let mailstore = { + accounts: {}, + saveMessage(address, message) { + if (!this.accounts[address]) { + this.accounts[address] = []; + } + this.accounts[address].push(message); + }, + getMail(address, callback) { + if (!this.accounts[address] || this.accounts[address].length === 0) { + let err = new Error('No mail for ' + address); + err.status = 404; + return callback(err); + } + simpleParser(this.accounts[address].shift(), (err, mail) => { + if (err) { + return callback(err.message || err); + } + callback(null, mail); + }); + } +}; + // Setup server let server = new SMTPServer({ @@ -74,8 +99,12 @@ let server = new SMTPServer({ // Handle message stream onData: (stream, session, callback) => { let hash = crypto.createHash('md5'); + let message = ''; stream.on('data', chunk => { hash.update(chunk); + if (/^keep/i.test(session.envelope.rcptTo[0].address)) { + message += chunk; + } }); stream.on('end', () => { let err; @@ -84,6 +113,12 @@ let server = new SMTPServer({ err.responseCode = 552; return callback(err); } + + // Store message for e2e tests + if (/^keep/i.test(session.envelope.rcptTo[0].address)) { + mailstore.saveMessage(session.envelope.rcptTo[0].address, message); + } + received++; callback(null, 'Message queued as ' + hash.digest('hex')); // accept the message once the stream is ended }); @@ -94,6 +129,41 @@ server.on('error', err => { log.error('Test SMTP', err.stack); }); +let mailBoxServer = http.createServer((req, res) => { + let renderer = data => ( + '' + data.title + '' + data.body + '' + ); + + let address = req.url.substring(1); + mailstore.getMail(address, (err, mail) => { + if (err) { + let html = renderer({ + title: 'error', + body: err.message || err + }); + res.writeHead(err.status || 500, { 'Content-Type': 'text/html' }); + return res.end(html); + } + + let html = mail.html || renderer({ + title: 'error', + body: 'This mail has no HTML part' + }); + + // https://nodemailer.com/extras/mailparser/#mail-object + delete mail.html; + delete mail.textAsHtml; + delete mail.attachments; + + let script = ''; + html = html.replace(/<\/body\b/i, match => script + match); + html = html.replace(/target="_blank"/g, 'target="_self"'); + + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(html); + }); +}); + module.exports = callback => { if (config.testserver.enabled) { server.listen(config.testserver.port, config.testserver.host, () => { @@ -112,7 +182,10 @@ module.exports = callback => { } }, 60 * 1000); - setImmediate(callback); + mailBoxServer.listen(config.testserver.mailboxserverport, config.testserver.host, () => { + log.info('Test SMTP', 'Mail Box Server listening on port %s', config.testserver.mailboxserverport); + setImmediate(callback); + }); }); } else { setImmediate(callback); diff --git a/setup/sql/drop.js b/setup/sql/drop.js index ba0291d2..3e972fb3 100644 --- a/setup/sql/drop.js +++ b/setup/sql/drop.js @@ -1,17 +1,23 @@ 'use strict'; -if (process.env.NODE_ENV === 'production') { - console.log('This script does not run in production'); // eslint-disable-line no-console - process.exit(1); -} - let config = require('config'); let spawn = require('child_process').spawn; let log = require('npmlog'); let path = require('path'); +let fs = require('fs'); log.level = 'verbose'; +if (process.env.NODE_ENV === 'production') { + log.error('sqldrop', 'This script does not run in production'); + process.exit(1); +} + +if (process.env.NODE_ENV === 'test' && !fs.existsSync(path.join(__dirname, '..', '..', 'config', 'test.toml'))) { + log.error('sqldrop', 'This script only runs in test if config/test.toml (i.e. a dedicated test database) is present'); + process.exit(1); +} + function createDump(callback) { let cmd = spawn(path.join(__dirname, 'drop.sh'), [], { env: { diff --git a/setup/sql/init.js b/setup/sql/init.js index c3a638e3..c654b451 100644 --- a/setup/sql/init.js +++ b/setup/sql/init.js @@ -1,15 +1,22 @@ 'use strict'; -if (process.env.NODE_ENV === 'production') { - console.log('This script does not run in production'); // eslint-disable-line no-console - process.exit(1); -} - let dbcheck = require('../../lib/dbcheck'); let log = require('npmlog'); +let path = require('path'); +let fs = require('fs'); log.level = 'verbose'; +if (process.env.NODE_ENV === 'production') { + log.error('sqlinit', 'This script does not run in production'); + process.exit(1); +} + +if (process.env.NODE_ENV === 'test' && !fs.existsSync(path.join(__dirname, '..', '..', 'config', 'test.toml'))) { + log.error('sqlinit', 'This script only runs in test if config/test.toml (i.e. a dedicated test database) is present'); + process.exit(1); +} + dbcheck(err => { if (err) { log.error('DB', err); diff --git a/setup/sql/mailtrain-test.sql b/setup/sql/mailtrain-test.sql new file mode 100644 index 00000000..8ae97db9 --- /dev/null +++ b/setup/sql/mailtrain-test.sql @@ -0,0 +1,1023 @@ +SET UNIQUE_CHECKS=0; +SET FOREIGN_KEY_CHECKS=0; + +CREATE TABLE `attachments` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `campaign` int(11) unsigned NOT NULL, + `filename` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '', + `content_type` varchar(100) CHARACTER SET ascii NOT NULL DEFAULT '', + `content` longblob, + `size` int(11) NOT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `campaign` (`campaign`), + CONSTRAINT `attachments_ibfk_1` FOREIGN KEY (`campaign`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `blacklist` ( + `email` varchar(191) NOT NULL, + PRIMARY KEY (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `campaign` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `list` int(11) unsigned NOT NULL, + `segment` int(11) unsigned NOT NULL, + `subscription` int(11) unsigned NOT NULL, + `status` tinyint(4) unsigned NOT NULL DEFAULT '0', + `response` varchar(255) DEFAULT NULL, + `response_id` varchar(255) CHARACTER SET ascii DEFAULT NULL, + `updated` timestamp NULL DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `list` (`list`,`segment`,`subscription`), + KEY `created` (`created`), + KEY `response_id` (`response_id`), + KEY `status_index` (`status`), + KEY `subscription_index` (`subscription`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `campaign_tracker` ( + `list` int(11) unsigned NOT NULL, + `subscriber` int(11) unsigned NOT NULL, + `link` int(11) NOT NULL, + `ip` varchar(100) CHARACTER SET ascii DEFAULT NULL, + `device_type` varchar(50) DEFAULT NULL, + `country` varchar(2) CHARACTER SET ascii DEFAULT NULL, + `count` int(11) unsigned NOT NULL DEFAULT '1', + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`list`,`subscriber`,`link`), + KEY `created_index` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `campaigns` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `cid` varchar(255) CHARACTER SET ascii NOT NULL, + `type` tinyint(4) unsigned NOT NULL DEFAULT '1', + `parent` int(11) unsigned DEFAULT NULL, + `name` varchar(255) NOT NULL DEFAULT '', + `description` text, + `list` int(11) unsigned NOT NULL, + `segment` int(11) unsigned DEFAULT NULL, + `template` int(11) unsigned NOT NULL, + `source_url` varchar(255) CHARACTER SET ascii DEFAULT NULL, + `editor_name` varchar(50) DEFAULT '', + `editor_data` longtext, + `last_check` timestamp NULL DEFAULT NULL, + `check_status` varchar(255) DEFAULT NULL, + `from` varchar(255) DEFAULT '', + `address` varchar(255) DEFAULT '', + `reply_to` varchar(255) DEFAULT '', + `subject` varchar(255) DEFAULT '', + `html` longtext, + `html_prepared` longtext, + `text` longtext, + `status` tinyint(4) unsigned NOT NULL DEFAULT '1', + `tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', + `scheduled` timestamp NULL DEFAULT NULL, + `status_change` timestamp NULL DEFAULT NULL, + `delivered` int(11) unsigned NOT NULL DEFAULT '0', + `blacklisted` int(11) unsigned NOT NULL DEFAULT '0', + `opened` int(11) unsigned NOT NULL DEFAULT '0', + `clicks` int(11) unsigned NOT NULL DEFAULT '0', + `unsubscribed` int(11) unsigned NOT NULL DEFAULT '0', + `bounced` int(1) unsigned NOT NULL DEFAULT '0', + `complained` int(1) unsigned NOT NULL DEFAULT '0', + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `cid` (`cid`), + KEY `name` (`name`(191)), + KEY `status` (`status`), + KEY `schedule_index` (`scheduled`), + KEY `type_index` (`type`), + KEY `parent_index` (`parent`), + KEY `check_index` (`last_check`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `confirmations` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `cid` varchar(255) CHARACTER SET ascii NOT NULL, + `list` int(11) unsigned NOT NULL, + `email` varchar(255) NOT NULL, + `opt_in_ip` varchar(100) DEFAULT NULL, + `data` text NOT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `cid` (`cid`), + KEY `list` (`list`), + CONSTRAINT `confirmations_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `custom_fields` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `list` int(11) unsigned NOT NULL, + `name` varchar(255) DEFAULT '', + `key` varchar(100) CHARACTER SET ascii NOT NULL, + `default_value` varchar(255) DEFAULT NULL, + `type` varchar(255) CHARACTER SET ascii NOT NULL DEFAULT '', + `group` int(11) unsigned DEFAULT NULL, + `group_template` text, + `column` varchar(255) CHARACTER SET ascii DEFAULT NULL, + `visible` tinyint(4) unsigned NOT NULL DEFAULT '1', + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `list` (`list`,`column`), + KEY `list_2` (`list`), + CONSTRAINT `custom_fields_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `custom_forms` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `list` int(11) unsigned NOT NULL, + `name` varchar(255) DEFAULT '', + `description` text, + `fields_shown_on_subscribe` varchar(255) DEFAULT '', + `fields_shown_on_manage` varchar(255) DEFAULT '', + `layout` longtext, + `form_input_style` longtext, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `list` (`list`), + CONSTRAINT `custom_forms_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `custom_forms_data` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `form` int(11) unsigned NOT NULL, + `data_key` varchar(255) DEFAULT '', + `data_value` longtext, + PRIMARY KEY (`id`), + KEY `form` (`form`), + CONSTRAINT `custom_forms_data_ibfk_1` FOREIGN KEY (`form`) REFERENCES `custom_forms` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `import_failed` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `import` int(11) unsigned NOT NULL, + `email` varchar(255) NOT NULL DEFAULT '', + `reason` varchar(255) DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `import` (`import`), + CONSTRAINT `import_failed_ibfk_1` FOREIGN KEY (`import`) REFERENCES `importer` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `importer` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `list` int(11) unsigned NOT NULL, + `type` tinyint(4) unsigned NOT NULL DEFAULT '1', + `path` varchar(255) NOT NULL DEFAULT '', + `size` int(11) unsigned NOT NULL DEFAULT '0', + `delimiter` varchar(1) CHARACTER SET ascii NOT NULL DEFAULT ',', + `emailcheck` tinyint(4) unsigned NOT NULL DEFAULT '1', + `status` tinyint(4) unsigned NOT NULL DEFAULT '0', + `error` varchar(255) DEFAULT NULL, + `processed` int(11) unsigned NOT NULL DEFAULT '0', + `new` int(11) unsigned NOT NULL DEFAULT '0', + `failed` int(11) unsigned NOT NULL DEFAULT '0', + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `mapping` text NOT NULL, + `finished` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `list` (`list`), + CONSTRAINT `importer_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `links` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `cid` varchar(255) CHARACTER SET ascii NOT NULL DEFAULT '', + `campaign` int(11) unsigned NOT NULL, + `url` varchar(255) CHARACTER SET ascii NOT NULL DEFAULT '', + `clicks` int(11) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `cid` (`cid`), + UNIQUE KEY `campaign_2` (`campaign`,`url`), + KEY `campaign` (`campaign`), + CONSTRAINT `links_ibfk_1` FOREIGN KEY (`campaign`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `lists` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `cid` varchar(255) CHARACTER SET ascii NOT NULL, + `default_form` int(11) unsigned DEFAULT NULL, + `name` varchar(255) NOT NULL DEFAULT '', + `description` text, + `subscribers` int(11) unsigned DEFAULT '0', + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `public_subscribe` tinyint(1) unsigned NOT NULL DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `cid` (`cid`), + KEY `name` (`name`(191)) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; +INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`) VALUES (1,'Hkj1vCoJb',NULL,'01 Testlist - Public Subscribe','',0,NOW(),1); +CREATE TABLE `queued` ( + `campaign` int(11) unsigned NOT NULL, + `list` int(11) unsigned NOT NULL, + `subscriber` int(11) unsigned NOT NULL, + `source` varchar(255) DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`campaign`,`list`,`subscriber`), + KEY `created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `report_templates` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT '', + `mime_type` varchar(255) NOT NULL DEFAULT 'text/html', + `description` text, + `user_fields` longtext, + `js` longtext, + `hbs` longtext, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `reports` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT '', + `description` text, + `report_template` int(11) unsigned NOT NULL, + `params` longtext, + `state` int(11) unsigned NOT NULL DEFAULT '0', + `last_run` datetime DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `report_template` (`report_template`), + CONSTRAINT `report_template_ibfk_1` FOREIGN KEY (`report_template`) REFERENCES `report_templates` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `rss` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `parent` int(11) unsigned NOT NULL, + `guid` varchar(255) NOT NULL DEFAULT '', + `pubdate` timestamp NULL DEFAULT NULL, + `campaign` int(11) unsigned DEFAULT NULL, + `found` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `parent_2` (`parent`,`guid`), + KEY `parent` (`parent`), + CONSTRAINT `rss_ibfk_1` FOREIGN KEY (`parent`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `segment_rules` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `segment` int(11) unsigned NOT NULL, + `column` varchar(255) CHARACTER SET ascii NOT NULL DEFAULT '', + `value` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + KEY `segment` (`segment`), + CONSTRAINT `segment_rules_ibfk_1` FOREIGN KEY (`segment`) REFERENCES `segments` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `segments` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `list` int(11) unsigned NOT NULL, + `name` varchar(255) NOT NULL DEFAULT '', + `type` tinyint(4) unsigned NOT NULL, + `created` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `list` (`list`), + KEY `name` (`name`(191)), + CONSTRAINT `segments_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `settings` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `key` varchar(255) CHARACTER SET ascii NOT NULL DEFAULT '', + `value` text NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key` (`key`) +) ENGINE=InnoDB AUTO_INCREMENT=112 DEFAULT CHARSET=utf8mb4; +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (1,'smtp_hostname','localhost'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (2,'smtp_port','5587'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','NONE'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (4,'smtp_user','testuser'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (5,'smtp_pass','testpass'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (6,'service_url','http://localhost:3000/'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (7,'admin_email','admin@example.com'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (8,'smtp_max_connections','5'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (9,'smtp_max_messages','100'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (10,'smtp_log',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (11,'default_sender','My Awesome Company'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (12,'default_postaddress','1234 Main Street'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (13,'default_from','My Awesome Company'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (14,'default_address','admin@example.com'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (15,'default_subject','Test message'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (16,'default_homepage','https://mailtrain.org'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','27'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (46,'ua_code',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (47,'shoutout',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (54,'mail_transport','smtp'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (60,'ses_key',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (61,'ses_secret',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (62,'ses_region','us-east-1'); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (65,'smtp_throttling',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (66,'pgp_passphrase',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (67,'pgp_private_key',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (68,'dkim_api_key',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (69,'dkim_domain',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (70,'dkim_selector',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (71,'dkim_private_key',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (73,'smtp_self_signed',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (74,'smtp_disable_auth',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (75,'verp_use',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (76,'disable_wysiwyg',''); +INSERT INTO `settings` (`id`, `key`, `value`) VALUES (77,'disable_confirmations',''); +CREATE TABLE `subscription` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `cid` varchar(255) CHARACTER SET ascii NOT NULL, + `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', + `opt_in_ip` varchar(100) DEFAULT NULL, + `opt_in_country` varchar(2) DEFAULT NULL, + `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL, + `imported` int(11) unsigned DEFAULT NULL, + `status` tinyint(4) unsigned NOT NULL DEFAULT '1', + `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0', + `status_change` timestamp NULL DEFAULT NULL, + `latest_open` timestamp NULL DEFAULT NULL, + `latest_click` timestamp NULL DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `first_name` varchar(255) DEFAULT NULL, + `last_name` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + UNIQUE KEY `cid` (`cid`), + KEY `status` (`status`), + KEY `first_name` (`first_name`(191)), + KEY `last_name` (`last_name`(191)), + KEY `subscriber_tz` (`tz`), + KEY `is_test` (`is_test`), + KEY `latest_open` (`latest_open`), + KEY `latest_click` (`latest_click`), + KEY `created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `subscription__1` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `cid` varchar(255) CHARACTER SET ascii NOT NULL, + `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', + `opt_in_ip` varchar(100) DEFAULT NULL, + `opt_in_country` varchar(2) DEFAULT NULL, + `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL, + `imported` int(11) unsigned DEFAULT NULL, + `status` tinyint(4) unsigned NOT NULL DEFAULT '1', + `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0', + `status_change` timestamp NULL DEFAULT NULL, + `latest_open` timestamp NULL DEFAULT NULL, + `latest_click` timestamp NULL DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `first_name` varchar(255) DEFAULT NULL, + `last_name` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + UNIQUE KEY `cid` (`cid`), + KEY `status` (`status`), + KEY `first_name` (`first_name`(191)), + KEY `last_name` (`last_name`(191)), + KEY `subscriber_tz` (`tz`), + KEY `is_test` (`is_test`), + KEY `latest_open` (`latest_open`), + KEY `latest_click` (`latest_click`), + KEY `created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `templates` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL DEFAULT '', + `description` text, + `editor_name` varchar(50) DEFAULT '', + `editor_data` longtext, + `html` longtext, + `text` longtext, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `name` (`name`(191)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `trigger` ( + `list` int(11) unsigned NOT NULL, + `subscription` int(11) unsigned NOT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`list`,`subscription`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `triggers` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL DEFAULT '', + `description` text, + `enabled` tinyint(4) unsigned NOT NULL DEFAULT '1', + `list` int(11) unsigned NOT NULL, + `source_campaign` int(11) unsigned DEFAULT NULL, + `rule` varchar(255) CHARACTER SET ascii NOT NULL DEFAULT 'column', + `column` varchar(255) CHARACTER SET ascii DEFAULT NULL, + `seconds` int(11) NOT NULL DEFAULT '0', + `dest_campaign` int(11) unsigned DEFAULT NULL, + `count` int(11) unsigned NOT NULL DEFAULT '0', + `last_check` timestamp NULL DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `name` (`name`(191)), + KEY `source_campaign` (`source_campaign`), + KEY `dest_campaign` (`dest_campaign`), + KEY `list` (`list`), + KEY `column` (`column`), + KEY `active` (`enabled`), + KEY `last_check` (`last_check`), + CONSTRAINT `triggers_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE `tzoffset` ( + `tz` varchar(100) NOT NULL DEFAULT '', + `offset` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`tz`) +) ENGINE=InnoDB DEFAULT CHARSET=ascii; +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/abidjan',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/accra',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/addis_ababa',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/algiers',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/asmara',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/asmera',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/bamako',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/bangui',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/banjul',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/bissau',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/blantyre',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/brazzaville',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/bujumbura',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/cairo',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/casablanca',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/ceuta',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/conakry',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/dakar',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/dar_es_salaam',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/djibouti',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/douala',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/el_aaiun',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/freetown',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/gaborone',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/harare',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/johannesburg',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/juba',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/kampala',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/khartoum',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/kigali',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/kinshasa',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/lagos',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/libreville',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/lome',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/luanda',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/lubumbashi',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/lusaka',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/malabo',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/maputo',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/maseru',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/mbabane',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/mogadishu',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/monrovia',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/nairobi',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/ndjamena',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/niamey',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/nouakchott',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/ouagadougou',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/porto-novo',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/sao_tome',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/timbuktu',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/tripoli',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/tunis',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/windhoek',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/adak',-540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/anchorage',-480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/anguilla',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/antigua',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/araguaina',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/buenos_aires',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/catamarca',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/comodrivadavia',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/cordoba',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/jujuy',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/la_rioja',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/mendoza',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/rio_gallegos',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/salta',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/san_juan',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/san_luis',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/tucuman',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/ushuaia',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/aruba',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/asuncion',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/atikokan',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/atka',-540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/bahia',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/bahia_banderas',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/barbados',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/belem',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/belize',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/blanc-sablon',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/boa_vista',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/bogota',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/boise',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/buenos_aires',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/cambridge_bay',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/campo_grande',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/cancun',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/caracas',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/catamarca',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/cayenne',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/cayman',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/chicago',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/chihuahua',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/coral_harbour',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/cordoba',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/costa_rica',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/creston',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/cuiaba',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/curacao',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/danmarkshavn',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/dawson',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/dawson_creek',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/denver',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/detroit',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/dominica',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/edmonton',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/eirunepe',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/el_salvador',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/ensenada',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/fortaleza',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/fort_nelson',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/fort_wayne',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/glace_bay',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/godthab',-120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/goose_bay',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/grand_turk',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/grenada',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/guadeloupe',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/guatemala',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/guayaquil',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/guyana',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/halifax',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/havana',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/hermosillo',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/indianapolis',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/knox',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/marengo',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/petersburg',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/tell_city',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/vevay',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/vincennes',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/winamac',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indianapolis',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/inuvik',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/iqaluit',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/jamaica',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/jujuy',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/juneau',-480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/kentucky/louisville',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/kentucky/monticello',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/knox_in',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/kralendijk',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/la_paz',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/lima',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/los_angeles',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/louisville',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/lower_princes',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/maceio',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/managua',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/manaus',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/marigot',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/martinique',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/matamoros',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/mazatlan',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/mendoza',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/menominee',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/merida',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/metlakatla',-480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/mexico_city',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/miquelon',-120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/moncton',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/monterrey',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/montevideo',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/montreal',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/montserrat',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/nassau',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/new_york',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/nipigon',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/nome',-480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/noronha',-120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/north_dakota/beulah',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/north_dakota/center',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/north_dakota/new_salem',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/ojinaga',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/panama',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/pangnirtung',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/paramaribo',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/phoenix',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/port-au-prince',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/porto_acre',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/porto_velho',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/port_of_spain',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/puerto_rico',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/punta_arenas',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rainy_river',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rankin_inlet',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/recife',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/regina',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/resolute',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rio_branco',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rosario',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santarem',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santa_isabel',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santiago',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santo_domingo',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/sao_paulo',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/scoresbysund',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/shiprock',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/sitka',-480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/st_barthelemy',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/st_johns',-150); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/st_kitts',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/st_lucia',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/st_thomas',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/st_vincent',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/swift_current',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/tegucigalpa',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/thule',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/thunder_bay',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/tijuana',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/toronto',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/tortola',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/vancouver',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/virgin',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/whitehorse',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/winnipeg',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/yakutat',-480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/yellowknife',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/casey',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/davis',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/dumontdurville',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/macquarie',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/mawson',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/mcmurdo',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/palmer',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/rothera',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/south_pole',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/syowa',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/troll',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/vostok',360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('arctic/longyearbyen',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/aden',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/almaty',360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/amman',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/anadyr',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/aqtau',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/aqtobe',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ashgabat',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ashkhabad',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/atyrau',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/baghdad',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/bahrain',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/baku',240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/bangkok',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/barnaul',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/beirut',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/bishkek',360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/brunei',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/calcutta',330); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/chita',540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/choibalsan',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/chongqing',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/chungking',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/colombo',330); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/dacca',360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/damascus',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/dhaka',360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/dili',540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/dubai',240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/dushanbe',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/famagusta',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/gaza',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/harbin',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/hebron',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/hong_kong',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/hovd',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ho_chi_minh',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/irkutsk',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/istanbul',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/jakarta',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/jayapura',540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/jerusalem',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kabul',270); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kamchatka',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/karachi',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kashgar',360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kathmandu',345); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/katmandu',345); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/khandyga',540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kolkata',330); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/krasnoyarsk',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kuala_lumpur',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kuching',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kuwait',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/macao',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/macau',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/magadan',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/makassar',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/manila',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/muscat',240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/nicosia',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/novokuznetsk',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/novosibirsk',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/omsk',360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/oral',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/phnom_penh',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/pontianak',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/pyongyang',510); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/qatar',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/qyzylorda',360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/rangoon',390); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/riyadh',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/saigon',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/sakhalin',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/samarkand',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/seoul',540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/shanghai',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/singapore',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/srednekolymsk',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/taipei',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/tashkent',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/tbilisi',240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/tehran',270); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/tel_aviv',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/thimbu',360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/thimphu',360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/tokyo',540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/tomsk',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ujung_pandang',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ulaanbaatar',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ulan_bator',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/urumqi',360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ust-nera',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/vientiane',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/vladivostok',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/yakutsk',540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/yangon',390); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/yekaterinburg',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/yerevan',240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/azores',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/bermuda',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/canary',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/cape_verde',-60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/faeroe',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/faroe',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/jan_mayen',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/madeira',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/reykjavik',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/south_georgia',-120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/stanley',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/st_helena',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/act',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/adelaide',570); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/brisbane',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/broken_hill',570); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/canberra',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/currie',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/darwin',570); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/eucla',525); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/hobart',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/lhi',630); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/lindeman',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/lord_howe',630); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/melbourne',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/north',570); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/nsw',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/perth',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/queensland',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/south',570); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/sydney',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/tasmania',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/victoria',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/west',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/yancowinna',570); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('brazil/acre',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('brazil/denoronha',-120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('brazil/east',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('brazil/west',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/atlantic',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/central',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/east-saskatchewan',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/eastern',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/mountain',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/newfoundland',-150); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/pacific',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/saskatchewan',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/yukon',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cet',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/continental',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/easterisland',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cst6cdt',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cuba',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('eet',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('egypt',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('eire',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('est',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('est5edt',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+0',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+1',-60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+10',-600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+11',-660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+12',-720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+2',-120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+3',-180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+4',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+5',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+6',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+7',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+8',-480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+9',-540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-0',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-1',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-10',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-11',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-12',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-13',780); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-14',840); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-2',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-3',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-4',240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-5',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-6',360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-7',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-8',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-9',540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt0',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/greenwich',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/uct',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/universal',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/utc',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/zulu',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/amsterdam',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/andorra',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/astrakhan',240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/athens',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/belfast',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/belgrade',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/berlin',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/bratislava',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/brussels',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/bucharest',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/budapest',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/busingen',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/chisinau',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/copenhagen',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/dublin',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/gibraltar',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/guernsey',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/helsinki',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/isle_of_man',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/istanbul',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/jersey',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/kaliningrad',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/kiev',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/kirov',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/lisbon',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/ljubljana',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/london',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/luxembourg',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/madrid',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/malta',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/mariehamn',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/minsk',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/monaco',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/moscow',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/nicosia',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/oslo',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/paris',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/podgorica',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/prague',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/riga',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/rome',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/samara',240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/san_marino',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/sarajevo',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/saratov',240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/simferopol',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/skopje',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/sofia',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/stockholm',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/tallinn',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/tirane',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/tiraspol',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/ulyanovsk',240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/uzhgorod',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/vaduz',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/vatican',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/vienna',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/vilnius',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/volgograd',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/warsaw',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/zagreb',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/zaporozhye',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/zurich',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('gb',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('gb-eire',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('gmt',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('gmt+0',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('gmt-0',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('gmt0',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('greenwich',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('hongkong',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('hst',-600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('iceland',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/antananarivo',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/chagos',360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/christmas',420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/cocos',390); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/comoro',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/kerguelen',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/mahe',240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/maldives',300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/mauritius',240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/mayotte',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/reunion',240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('iran',270); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('israel',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('jamaica',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('japan',540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('kwajalein',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('libya',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('met',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('mexico/bajanorte',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('mexico/bajasur',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('mexico/general',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('mst',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('mst7mdt',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('navajo',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('nz',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('nz-chat',765); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/apia',780); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/auckland',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/bougainville',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/chatham',765); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/chuuk',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/easter',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/efate',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/enderbury',780); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/fakaofo',780); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/fiji',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/funafuti',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/galapagos',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/gambier',-540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/guadalcanal',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/guam',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/honolulu',-600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/johnston',-600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/kiritimati',840); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/kosrae',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/kwajalein',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/majuro',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/marquesas',-570); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/midway',-660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/nauru',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/niue',-660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/norfolk',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/noumea',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/pago_pago',-660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/palau',540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/pitcairn',-480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/pohnpei',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/ponape',660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/port_moresby',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/rarotonga',-600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/saipan',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/samoa',-660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/tahiti',-600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/tarawa',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/tongatapu',780); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/truk',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/wake',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/wallis',720); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/yap',600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('poland',120); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('portugal',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('prc',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pst8pdt',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('roc',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('rok',540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('singapore',480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('turkey',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('uct',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('universal',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/alaska',-480); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/aleutian',-540); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/arizona',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/central',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/east-indiana',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/eastern',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/hawaii',-600); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/indiana-starke',-300); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/michigan',-240); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/mountain',-360); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/pacific',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/pacific-new',-420); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/samoa',-660); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('utc',0); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('w-su',180); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('wet',60); +INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('zulu',0); +CREATE TABLE `users` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(255) NOT NULL DEFAULT '', + `password` varchar(255) NOT NULL DEFAULT '', + `email` varchar(255) CHARACTER SET utf8 DEFAULT NULL, + `access_token` varchar(40) DEFAULT NULL, + `reset_token` varchar(255) CHARACTER SET ascii DEFAULT NULL, + `reset_expire` timestamp NULL DEFAULT NULL, + `created` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + KEY `username` (`username`(191)), + KEY `reset` (`reset_token`), + KEY `check_reset` (`username`(191),`reset_token`,`reset_expire`), + KEY `token_index` (`access_token`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; +INSERT INTO `users` (`id`, `username`, `password`, `email`, `access_token`, `reset_token`, `reset_expire`, `created`) VALUES (1,'admin','$2a$10$mzKU71G62evnGB2PvQA4k..Wf9jASk.c7a8zRMHh6qQVjYJ2r/g/K','admin@example.com',NULL,NULL,NULL,NOW()); + +SET UNIQUE_CHECKS=1; +SET FOREIGN_KEY_CHECKS=1; diff --git a/test/e2e/.eslintrc b/test/e2e/.eslintrc new file mode 100644 index 00000000..836bac9a --- /dev/null +++ b/test/e2e/.eslintrc @@ -0,0 +1,11 @@ +{ + "parser": "babel-eslint", + "rules": { + "strict": 0, + "no-invalid-this": 0, + "no-unused-expressions": 0 + }, + "env": { + "mocha": true + } +} diff --git a/test/e2e/README.md b/test/e2e/README.md new file mode 100644 index 00000000..e4ed24fb --- /dev/null +++ b/test/e2e/README.md @@ -0,0 +1,44 @@ +# e2e Tests + +Running e2e tests requires Node 7.6 or later and a dedicated test database. It uses mocha, selenium-webdriver and phantomjs. + +## Installation + +These e2e tests have to be performed against predefined resources (e.g. lists, users, etc.) and therefore a dedicated test database and test config is required. + +Both can be created by running `sudo sh test/e2e/install.sh` from within your mailtrain directory. This creates a MYSQL user and database called `mailtrain_test`, and generates the required `config/test.toml`. + +## Running e2e Tests + +For tests to succeed Mailtrian must be started in `test` mode on port 3000 (as http://localhost:3000/ is the predefined service url). The tests itself have to be started in a second Terminal window. + +1. Start Mailtrain with `npm run starttest` +2. Start e2e tests with `npm run e2e` + +## Using Different Browsers + +By default e2e tests use `phantomjs`. If you want to use a different browser you need to install its driver and adjust your `config/test.toml`. + +* Install the `firefox` driver with `npm install geckodriver` +* Install the `chrome` driver with `npm install chromedriver` +* Other drivers can be found [here](https://seleniumhq.github.io/selenium/docs/api/javascript/) + +Then adjust your config: + +``` +[seleniumwebdriver] +browser="firefox" +``` + +Current Firefox issue (and patch): https://github.com/mozilla/geckodriver/issues/683 + +## Writing e2e Tests + +You should spend your time on features rather than writing tests, yet in some cases, like for example the subscription process, manual testing is just silly. You best get started by reading the current test suites, or just open an issue describing the scenario you want to get tested. + +Available commands: + +* `npm run sqldumptest` - exports the test DB to `setup/sql/mailtrain-test.sql` +* `npm run sqlresettest` - drops all tables then loads `setup/sql/mailtrain-test.sql` +* `npm run _e2e` - just runs e2e tests +* `npm run e2e` - runs `sqlresettest` then `_e2e` diff --git a/test/e2e/helpers/config.js b/test/e2e/helpers/config.js new file mode 100644 index 00000000..71726ff7 --- /dev/null +++ b/test/e2e/helpers/config.js @@ -0,0 +1,31 @@ +'use strict'; + +const config = require('config'); + +module.exports = { + app: config, + baseUrl: 'http://localhost:' + config.www.port, + users: { + admin: { + username: 'admin', + password: 'test' + } + }, + lists: { + one: { + id: 1, + cid: 'Hkj1vCoJb', + publicSubscribe: 1, + unsubscriptionMode: 0 + } + }, + settings: { + 'service-url' : 'http://localhost:' + config.www.port + '/', + 'default-homepage': 'https://mailtrain.org', + 'smtp-hostname': config.testserver.host, + 'smtp-port': config.testserver.port, + 'smtp-encryption': 'NONE', + 'smtp-user': config.testserver.username, + 'smtp-pass': config.testserver.password + } +}; diff --git a/test/e2e/helpers/driver.js b/test/e2e/helpers/driver.js new file mode 100644 index 00000000..a9b8444b --- /dev/null +++ b/test/e2e/helpers/driver.js @@ -0,0 +1,15 @@ +'use strict'; + +const config = require('./config'); +const webdriver = require('selenium-webdriver'); + +const driver = new webdriver.Builder() + .forBrowser(config.app.seleniumwebdriver.browser || 'phantomjs') + .build(); + +if (global.USE_SHARED_DRIVER === true) { + driver.originalQuit = driver.quit; + driver.quit = () => {}; +} + +module.exports = driver; diff --git a/test/e2e/helpers/exit-unless-test.js b/test/e2e/helpers/exit-unless-test.js new file mode 100644 index 00000000..63c02761 --- /dev/null +++ b/test/e2e/helpers/exit-unless-test.js @@ -0,0 +1,21 @@ +'use strict'; + +const config = require('./config'); +const log = require('npmlog'); +const path = require('path'); +const fs = require('fs'); + +if (process.env.NODE_ENV !== 'test' || !fs.existsSync(path.join(__dirname, '..', '..', '..', 'config', 'test.toml'))) { + log.error('e2e', 'This script only runs in test and config/test.toml (i.e. a dedicated test database) is present'); + process.exit(1); +} + +if (config.app.testserver.enabled !== true) { + log.error('e2e', 'This script only runs if the testserver is enabled. Check config/test.toml'); + process.exit(1); +} + +if (config.app.www.port !== 3000) { + log.error('e2e', 'This script requires Mailtrain to be running on port 3000. Check config/test.toml'); + process.exit(1); +} diff --git a/test/e2e/index.js b/test/e2e/index.js new file mode 100644 index 00000000..d78f5819 --- /dev/null +++ b/test/e2e/index.js @@ -0,0 +1,36 @@ +'use strict'; + +require('./helpers/exit-unless-test'); + +global.USE_SHARED_DRIVER = true; + +const driver = require('./helpers/driver'); +const only = 'only'; +const skip = 'skip'; + + + +let tests = [ + ['tests/login'], + ['tests/subscription'] +]; + + + +tests = tests.filter(t => t[1] !== skip); + +if (tests.some(t => t[1] === only)) { + tests = tests.filter(t => t[1] === only); +} + +describe('e2e', function() { + this.timeout(10000); + + tests.forEach(t => { + describe(t[0], () => { + require('./' + t[0]); // eslint-disable-line global-require + }); + }); + + after(() => driver.originalQuit()); +}); diff --git a/test/e2e/install.sh b/test/e2e/install.sh new file mode 100644 index 00000000..860632aa --- /dev/null +++ b/test/e2e/install.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# This installation script works on Ubuntu 14.04 and 16.04 +# Run as root! + +if [[ $EUID -ne 0 ]]; then + echo "This script must be run as root" 1>&2 + exit 1 +fi + +set -e + +export DEBIAN_FRONTEND=noninteractive + +MYSQL_PASSWORD=`pwgen 12 -1` + +# Setup MySQL user for Mailtrain Tests +mysql -u root -e "CREATE USER 'mailtrain_test'@'localhost' IDENTIFIED BY '$MYSQL_PASSWORD';" +mysql -u root -e "GRANT ALL PRIVILEGES ON mailtrain_test.* TO 'mailtrain_test'@'localhost';" +mysql -u mailtrain_test --password="$MYSQL_PASSWORD" -e "CREATE database mailtrain_test;" + +# Setup installation configuration +cat >> config/test.toml < Object.assign(page(driver), { + elementToWaitFor: 'alert', + elements: { + alert: 'div.alert:not(.js-warning)' + }, + getText() { + return this.element('alert').getText(); + }, + clear() { + return this.driver.executeScript(` + var elements = document.getElementsByClassName('alert'); + while(elements.length > 0){ + elements[0].parentNode.removeChild(elements[0]); + } + `); + } +}); diff --git a/test/e2e/page-objects/home.js b/test/e2e/page-objects/home.js new file mode 100644 index 00000000..2fccae24 --- /dev/null +++ b/test/e2e/page-objects/home.js @@ -0,0 +1,11 @@ +'use strict'; + +const page = require('./page'); + +module.exports = driver => Object.assign(page(driver), { + url: '/', + elementToWaitFor: 'body', + elements: { + body: 'body.page--home' + } +}); diff --git a/test/e2e/page-objects/page.js b/test/e2e/page-objects/page.js new file mode 100644 index 00000000..ece926ad --- /dev/null +++ b/test/e2e/page-objects/page.js @@ -0,0 +1,55 @@ +'use strict'; + +const config = require('../helpers/config'); +const webdriver = require('selenium-webdriver'); +const By = webdriver.By; +const until = webdriver.until; + +module.exports = driver => ({ + driver, + elements: {}, + + element(key) { + return this.driver.findElement(By.css(this.elements[key] || key)); + }, + + navigate(path) { + this.driver.navigate().to(config.baseUrl + (path || this.url)); + return this.waitUntilVisible(); + }, + + waitUntilVisible() { + let selector = this.elements[this.elementToWaitFor]; + if (!selector && this.url) { + selector = 'body.page--' + (this.url.substring(1).replace(/\//g, '--') || 'home'); + } + return selector ? this.driver.wait(until.elementLocated(By.css(selector))) : this.driver.sleep(1000); + }, + + submit() { + return this.element('submitButton').click(); + }, + + click(key) { + return this.element(key).click(); + }, + + getText(key) { + return this.element(key).getText(); + }, + + getValue(key) { + return this.element(key).getAttribute('value'); + }, + + setValue(key, value) { + return this.element(key).sendKeys(value); + }, + + containsText(str) { + // let text = await driver.findElement({ css: 'body' }).getText(); + return this.driver.executeScript(` + return (document.documentElement.textContent || document.documentElement.innerText).indexOf('${str}') > -1; + `); + } +}); diff --git a/test/e2e/page-objects/subscription.js b/test/e2e/page-objects/subscription.js new file mode 100644 index 00000000..20b56f6f --- /dev/null +++ b/test/e2e/page-objects/subscription.js @@ -0,0 +1,84 @@ +'use strict'; + +const config = require('../helpers/config'); +const page = require('./page'); + +const web = { + enterEmail(value) { + this.element('emailInput').clear(); + return this.element('emailInput').sendKeys(value); + } +}; + +const mail = { + navigate(address) { + this.driver.sleep(100); + this.driver.navigate().to(`http://localhost:${config.app.testserver.mailboxserverport}/${address}`); + return this.waitUntilVisible(); + } +}; + +module.exports = (driver, list) => ({ + + webSubscribe: Object.assign(page(driver), web, { + url: `/subscription/${list.cid}`, + elementToWaitFor: 'form', + elements: { + form: `form[action="/subscription/${list.cid}/subscribe"]`, + emailInput: '#main-form input[name="email"]', + submitButton: 'a[href="#submit"]' + } + }), + + webConfirmSubscriptionNotice: Object.assign(page(driver), web, { + url: `/subscription/${list.cid}/confirm-notice`, + elementToWaitFor: 'homepageButton', + elements: { + homepageButton: `a[href="${config.settings['default-homepage']}"]` + } + }), + + mailConfirmSubscription: Object.assign(page(driver), mail, { + elementToWaitFor: 'confirmLink', + elements: { + confirmLink: `a[href^="${config.settings['service-url']}subscription/subscribe/"]` + } + }), + + webSubscribedNotice: Object.assign(page(driver), web, { + elementToWaitFor: 'homepageButton', + elements: { + homepageButton: 'a[href^="https://mailtrain.org"]' + } + }), + + mailSubscriptionConfirmed: Object.assign(page(driver), mail, { + elementToWaitFor: 'unsubscribeLink', + elements: { + unsubscribeLink: 'a[href*="/unsubscribe/"]', + manageLink: 'a[href*="/manage/"]' + } + }), + + webUnsubscribe: Object.assign(page(driver), web, { + elementToWaitFor: 'submitButton', + elements: { + submitButton: 'a[href="#submit"]' + } + }), + + webUnsubscribedNotice: Object.assign(page(driver), web, { + elementToWaitFor: 'homepageButton', + elements: { + homepageButton: 'a[href^="https://mailtrain.org"]' + } + }), + + mailUnsubscriptionConfirmed: Object.assign(page(driver), mail, { + elementToWaitFor: 'resubscribeLink', + elements: { + resubscribeLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}"]` + } + }) + +}); diff --git a/test/e2e/page-objects/users.js b/test/e2e/page-objects/users.js new file mode 100644 index 00000000..27f7d82b --- /dev/null +++ b/test/e2e/page-objects/users.js @@ -0,0 +1,32 @@ +'use strict'; + +const page = require('./page'); + +module.exports = driver => ({ + + login: Object.assign(page(driver), { + url: '/users/login', + elementToWaitFor: 'submitButton', + elements: { + usernameInput: 'form[action="/users/login"] input[name="username"]', + passwordInput: 'form[action="/users/login"] input[name="password"]', + submitButton: 'form[action="/users/login"] [type=submit]' + }, + enterUsername(value) { + // this.element('usernameInput').clear(); + return this.element('usernameInput').sendKeys(value); + }, + enterPassword(value) { + return this.element('passwordInput').sendKeys(value); + } + }), + + account: Object.assign(page(driver), { + url: '/users/account', + elementToWaitFor: 'emailInput', + elements: { + emailInput: 'form[action="/users/account"] input[name="email"]' + } + }) + +}); diff --git a/test/e2e/tests/login.js b/test/e2e/tests/login.js new file mode 100644 index 00000000..238b5def --- /dev/null +++ b/test/e2e/tests/login.js @@ -0,0 +1,57 @@ +'use strict'; + +const config = require('../helpers/config'); +const expect = require('chai').expect; +const driver = require('../helpers/driver'); +const home = require('../page-objects/home')(driver); +const flash = require('../page-objects/flash')(driver); +const { + login, + account +} = require('../page-objects/users')(driver); + +describe('login', function() { + this.timeout(10000); + + before(() => driver.manage().deleteAllCookies()); + + it('can access home page', async () => { + await home.navigate(); + }); + + it('can not access restricted content', async () => { + driver.navigate().to(config.baseUrl + '/settings'); + flash.waitUntilVisible(); + expect(await flash.getText()).to.contain('Need to be logged in to access restricted content'); + await flash.clear(); + }); + + it('can not login with false credentials', async () => { + login.enterUsername(config.users.admin.username); + login.enterPassword('invalid'); + login.submit(); + flash.waitUntilVisible(); + expect(await flash.getText()).to.contain('Incorrect username or password'); + await flash.clear(); + }); + + it('can login as admin', async () => { + login.enterUsername(config.users.admin.username); + login.enterPassword(config.users.admin.password); + login.submit(); + flash.waitUntilVisible(); + expect(await flash.getText()).to.contain('Logged in as admin'); + }); + + it('can access account page as admin', async () => { + await account.navigate(); + }); + + it('can logout', async () => { + driver.navigate().to(config.baseUrl + '/users/logout'); + flash.waitUntilVisible(); + expect(await flash.getText()).to.contain('logged out'); + }); + + after(() => driver.quit()); +}); diff --git a/test/e2e/tests/subscription.js b/test/e2e/tests/subscription.js new file mode 100644 index 00000000..2e694aa8 --- /dev/null +++ b/test/e2e/tests/subscription.js @@ -0,0 +1,101 @@ +'use strict'; + +const config = require('../helpers/config'); +const shortid = require('shortid'); +const expect = require('chai').expect; +const driver = require('../helpers/driver'); + +const page = require('../page-objects/page')(driver); +const flash = require('../page-objects/flash')(driver); + +const { + webSubscribe, + webConfirmSubscriptionNotice, + mailConfirmSubscription, + webSubscribedNotice, + mailSubscriptionConfirmed, + webUnsubscribe, + webUnsubscribedNotice, + mailUnsubscriptionConfirmed +} = require('../page-objects/subscription')(driver, config.lists.one); + +const testuser = { + email: 'keep.' + shortid.generate() + '@mailtrain.org' +}; + +// console.log(testuser.email); + +describe('subscribe (list one)', function() { + this.timeout(10000); + + before(() => driver.manage().deleteAllCookies()); + + it('visits web-subscribe', async () => { + await webSubscribe.navigate(); + }); + + it('submits invalid email (error)', async () => { + webSubscribe.enterEmail('foo@bar.nope'); + webSubscribe.submit(); + flash.waitUntilVisible(); + expect(await flash.getText()).to.contain('Invalid email address'); + }); + + it('submits valid email', async () => { + webSubscribe.enterEmail(testuser.email); + await webSubscribe.submit(); + }); + + it('sees web-confirm-subscription-notice', async () => { + webConfirmSubscriptionNotice.waitUntilVisible(); + expect(await page.containsText('Almost Finished')).to.be.true; + }); + + it('receives mail-confirm-subscription', async () => { + mailConfirmSubscription.navigate(testuser.email); + expect(await page.containsText('Please Confirm Subscription')).to.be.true; + }); + + it('clicks confirm subscription', async () => { + await mailConfirmSubscription.click('confirmLink'); + }); + + it('sees web-subscribed-notice', async () => { + webSubscribedNotice.waitUntilVisible(); + expect(await page.containsText('Subscription Confirmed')).to.be.true; + }); + + it('receives mail-subscription-confirmed', async () => { + mailSubscriptionConfirmed.navigate(testuser.email); + expect(await page.containsText('Subscription Confirmed')).to.be.true; + }); +}); + +describe('unsubscribe (list one)', function() { + this.timeout(10000); + + it('clicks unsubscribe', async () => { + await mailSubscriptionConfirmed.click('unsubscribeLink'); + }); + + it('sees web-unsubscribe', async () => { + webUnsubscribe.waitUntilVisible(); + expect(await page.containsText('Unsubscribe')).to.be.true; + }); + + it('clicks confirm unsubscription', async () => { + await webUnsubscribe.submit(); + }); + + it('sees web-unsubscribed-notice', async () => { + webUnsubscribedNotice.waitUntilVisible(); + expect(await page.containsText('Unsubscribe Successful')).to.be.true; + }); + + it('receives mail-unsubscription-confirmed', async () => { + mailUnsubscriptionConfirmed.navigate(testuser.email); + expect(await page.containsText('You Are Now Unsubscribed')).to.be.true; + }); + + after(() => driver.quit()); +}); diff --git a/test/frontmail-test.js b/test/nodeunit/frontmail-test.js similarity index 100% rename from test/frontmail-test.js rename to test/nodeunit/frontmail-test.js diff --git a/views/layout.hbs b/views/layout.hbs index 339e8006..73eff28a 100644 --- a/views/layout.hbs +++ b/views/layout.hbs @@ -35,7 +35,7 @@ - +