feat(email): write live email-sending config to redis
This commit is contained in:
Родитель
4998f2b2ba
Коммит
c6ad40270a
|
@ -40,6 +40,7 @@ script:
|
||||||
- if [ $DB == "mysql" ]; then ./scripts/start-travis-auth-db-mysql.sh; fi
|
- if [ $DB == "mysql" ]; then ./scripts/start-travis-auth-db-mysql.sh; fi
|
||||||
- npm run test-ci
|
- npm run test-ci
|
||||||
- npm run test-e2e
|
- npm run test-e2e
|
||||||
|
- npm run test-scripts
|
||||||
# Test fxa-auth-mailer
|
# Test fxa-auth-mailer
|
||||||
- grunt templates && git status -s | (! grep 'M lib/senders/templates/')
|
- grunt templates && git status -s | (! grep 'M lib/senders/templates/')
|
||||||
- grunt l10n-extract
|
- grunt l10n-extract
|
||||||
|
|
59
README.md
59
README.md
|
@ -134,6 +134,65 @@ can be overridden in two ways:
|
||||||
export CONFIG_FILES="~/fxa-content-server.json,~/fxa-db.json"
|
export CONFIG_FILES="~/fxa-content-server.json,~/fxa-db.json"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Email config
|
||||||
|
|
||||||
|
There is also some live config
|
||||||
|
loaded from Redis for the email service.
|
||||||
|
This config is stored as a JSON string
|
||||||
|
that looks like this
|
||||||
|
(every property is optional):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sendgrid": {
|
||||||
|
"percentage": 100,
|
||||||
|
"regex": "^.+@example\\.com$"
|
||||||
|
},
|
||||||
|
"socketlabs": {
|
||||||
|
"percentage": 100,
|
||||||
|
"regex": "^.+@example\\.org$"
|
||||||
|
},
|
||||||
|
"ses": {
|
||||||
|
"percentage": 10,
|
||||||
|
"regex": ".*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/email-config.js`
|
||||||
|
has been written to help
|
||||||
|
manage this config.
|
||||||
|
|
||||||
|
* To print the current live config to stdout:
|
||||||
|
|
||||||
|
```
|
||||||
|
node scripts/email-config read
|
||||||
|
```
|
||||||
|
|
||||||
|
* To set the live config from a JSON file on disk:
|
||||||
|
|
||||||
|
```
|
||||||
|
cat foo.json | node scripts/email-config write
|
||||||
|
```
|
||||||
|
|
||||||
|
* To set the live config from a string:
|
||||||
|
|
||||||
|
```
|
||||||
|
echo '{"sendgrid":{"percentage":10}}' | node scripts/email-config write
|
||||||
|
```
|
||||||
|
|
||||||
|
* To undo the last change:
|
||||||
|
|
||||||
|
```
|
||||||
|
node scripts/email-config revert
|
||||||
|
```
|
||||||
|
|
||||||
|
* To check the resolved config for a specific email address:
|
||||||
|
|
||||||
|
```
|
||||||
|
node scripts/email-config check foo@example.com
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
Firefox Accounts authorization is a complicated flow. You can get verbose logging by adjusting the log level in the `config.json` on your deployed instance. Add a stanza like:
|
Firefox Accounts authorization is a complicated flow. You can get verbose logging by adjusting the log level in the `config.json` on your deployed instance. Add a stanza like:
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"test": "VERIFIER_VERSION=0 MEMCACHE_METRICS_CONTEXT_ADDRESS=none NO_COVERAGE=1 scripts/test-local.sh",
|
"test": "VERIFIER_VERSION=0 MEMCACHE_METRICS_CONTEXT_ADDRESS=none NO_COVERAGE=1 scripts/test-local.sh",
|
||||||
"test-ci": "scripts/test-local.sh",
|
"test-ci": "scripts/test-local.sh",
|
||||||
"test-e2e": "NODE_ENV=dev mocha test/e2e",
|
"test-e2e": "NODE_ENV=dev mocha test/e2e",
|
||||||
|
"test-scripts": "mocha test/scripts",
|
||||||
"test-remote": "MAILER_HOST=restmail.net MAILER_PORT=80 CORS_ORIGIN=http://baz mocha --timeout=300000 test/remote"
|
"test-remote": "MAILER_HOST=restmail.net MAILER_PORT=80 CORS_ORIGIN=http://baz mocha --timeout=300000 test/remote"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const ROOT_DIR = '..'
|
||||||
|
const LIB_DIR = `${ROOT_DIR}/lib`
|
||||||
|
|
||||||
|
const config = require(`${ROOT_DIR}/config`).getProperties()
|
||||||
|
const log = require(`${LIB_DIR}/log`)(config.log.level, 'email-config')
|
||||||
|
const Promise = require(`${LIB_DIR}/promise`)
|
||||||
|
const redis = require(`${LIB_DIR}/redis`)({ ...config.redis, ...config.redis.email }, log)
|
||||||
|
|
||||||
|
if (! redis) {
|
||||||
|
console.error('redis is disabled in config, aborting')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMMANDS = {
|
||||||
|
read,
|
||||||
|
write,
|
||||||
|
revert,
|
||||||
|
check
|
||||||
|
}
|
||||||
|
const KEYS = {
|
||||||
|
current: 'config',
|
||||||
|
previous: 'config.previous'
|
||||||
|
}
|
||||||
|
|
||||||
|
const { argv } = process
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(() => redis.close())
|
||||||
|
|
||||||
|
async function main () {
|
||||||
|
try {
|
||||||
|
const command = argv[2]
|
||||||
|
switch (command) {
|
||||||
|
case 'read':
|
||||||
|
case 'write':
|
||||||
|
case 'revert':
|
||||||
|
assertArgs(0)
|
||||||
|
break
|
||||||
|
case 'check':
|
||||||
|
assertArgs(1)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
usageError()
|
||||||
|
}
|
||||||
|
const result = await COMMANDS[command](...argv.slice(3))
|
||||||
|
if (result) {
|
||||||
|
console.log(result)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertArgs (count) {
|
||||||
|
if (argv.length !== count + 3) {
|
||||||
|
usageError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function usageError () {
|
||||||
|
const scriptName = argv[1].substr(argv[1].indexOf('/scripts/') + 1)
|
||||||
|
throw new Error([
|
||||||
|
'Usage:',
|
||||||
|
`${scriptName} read - Read the current config to stdout`,
|
||||||
|
`${scriptName} write - Write the current config from stdin`,
|
||||||
|
`${scriptName} revert - Undo the last write or revert`,
|
||||||
|
`${scriptName} check <email address> - Check whether <email address> matches config`
|
||||||
|
].join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function read () {
|
||||||
|
const current = await redis.get(KEYS.current)
|
||||||
|
if (current) {
|
||||||
|
// Parse then stringify for pretty printing
|
||||||
|
return JSON.stringify(JSON.parse(current), null, ' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function write () {
|
||||||
|
// Parse then stringify for validation
|
||||||
|
const current = JSON.stringify(JSON.parse(await stdin()))
|
||||||
|
const previous = await redis.get(KEYS.current)
|
||||||
|
await redis.set(KEYS.current, current)
|
||||||
|
if (previous) {
|
||||||
|
await redis.set(KEYS.previous, previous)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stdin () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks = []
|
||||||
|
process.stdin.on('readable', () => {
|
||||||
|
const chunk = process.stdin.read()
|
||||||
|
if (chunk !== null) {
|
||||||
|
chunks.push(chunk)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
process.stdin.on('error', reject)
|
||||||
|
process.stdin.on('end', () => resolve(chunks.join('')))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revert () {
|
||||||
|
const previous = await redis.get(KEYS.previous)
|
||||||
|
const current = await redis.get(KEYS.current)
|
||||||
|
if (previous) {
|
||||||
|
await redis.set(KEYS.current, previous)
|
||||||
|
} else {
|
||||||
|
await redis.del(KEYS.current)
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
await redis.set(KEYS.previous, current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function check (emailAddress) {
|
||||||
|
const current = await redis.get(KEYS.current)
|
||||||
|
if (current) {
|
||||||
|
const config = JSON.parse(await redis.get(KEYS.current))
|
||||||
|
const result = Object.entries(config)
|
||||||
|
.filter(([ sender, senderConfig ]) => {
|
||||||
|
if (senderConfig.regex) {
|
||||||
|
return new RegExp(senderConfig.regex).test(emailAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.reduce((matches, [ sender, senderConfig ]) => {
|
||||||
|
matches[sender] = senderConfig
|
||||||
|
return matches
|
||||||
|
}, {})
|
||||||
|
return JSON.stringify(result, null, ' ')
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,11 +7,15 @@
|
||||||
const ROOT_DIR = '../../..'
|
const ROOT_DIR = '../../..'
|
||||||
|
|
||||||
const assert = require('insist')
|
const assert = require('insist')
|
||||||
|
const cp = require('child_process')
|
||||||
const mocks = require('../../mocks')
|
const mocks = require('../../mocks')
|
||||||
const P = require('bluebird')
|
const P = require('bluebird')
|
||||||
|
const path = require('path')
|
||||||
const proxyquire = require('proxyquire').noPreserveCache()
|
const proxyquire = require('proxyquire').noPreserveCache()
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
|
|
||||||
|
cp.execAsync = P.promisify(cp.exec)
|
||||||
|
|
||||||
const config = require(`${ROOT_DIR}/config`)
|
const config = require(`${ROOT_DIR}/config`)
|
||||||
|
|
||||||
const TEMPLATE_VERSIONS = require(`${ROOT_DIR}/lib/senders/templates/_versions.json`)
|
const TEMPLATE_VERSIONS = require(`${ROOT_DIR}/lib/senders/templates/_versions.json`)
|
||||||
|
@ -1873,3 +1877,66 @@ describe('call selectEmailServices with mocked safe-regex, regex-only match and
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (config.get('redis.email.enabled')) {
|
||||||
|
const emailAddress = 'foo@example.com';
|
||||||
|
|
||||||
|
[ 'sendgrid', 'ses', 'socketlabs' ].reduce((promise, service) => {
|
||||||
|
return promise.then(() => {
|
||||||
|
return new P((resolve, reject) => {
|
||||||
|
describe(`call selectEmailServices with real redis containing ${service} config:`, () => {
|
||||||
|
let mailer, result
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
return P.all([
|
||||||
|
require(`${ROOT_DIR}/lib/senders/translator`)(['en'], 'en'),
|
||||||
|
require(`${ROOT_DIR}/lib/senders/templates`).init()
|
||||||
|
]).spread((translator, templates) => {
|
||||||
|
const mockLog = mocks.mockLog()
|
||||||
|
const Mailer = require(`${ROOT_DIR}/lib/senders/email`)(mockLog, config.getProperties())
|
||||||
|
mailer = new Mailer(translator, templates, config.get('smtp'))
|
||||||
|
return redisWrite({
|
||||||
|
[service]: {
|
||||||
|
regex: '^foo@example\.com$',
|
||||||
|
percentage: 100
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(() => mailer.selectEmailServices({ email: emailAddress }))
|
||||||
|
.then(r => result = r)
|
||||||
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
return redisRevert()
|
||||||
|
.then(() => mailer.stop())
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returned the correct result', () => {
|
||||||
|
assert.deepEqual(result, [
|
||||||
|
{
|
||||||
|
emailAddresses: [ emailAddress ],
|
||||||
|
emailService: 'fxa-email-service',
|
||||||
|
emailSender: service,
|
||||||
|
mailer: mailer.emailService
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, P.resolve())
|
||||||
|
}
|
||||||
|
|
||||||
|
function redisWrite (config) {
|
||||||
|
return cp.execAsync(`echo '${JSON.stringify(config)}' | node scripts/email-config write`, {
|
||||||
|
cwd: path.resolve(__dirname, '../../..')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function redisRevert () {
|
||||||
|
return cp.execAsync('node scripts/email-config revert', {
|
||||||
|
cwd: path.resolve(__dirname, '../../..')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,224 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const ROOT_DIR = '../..'
|
||||||
|
const LIB_DIR = `${ROOT_DIR}/lib`
|
||||||
|
|
||||||
|
const assert = require('insist')
|
||||||
|
const cp = require('child_process')
|
||||||
|
const mocks = require(`${ROOT_DIR}/test/mocks`)
|
||||||
|
const P = require('bluebird')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
cp.execAsync = P.promisify(cp.exec)
|
||||||
|
|
||||||
|
const config = require(`${ROOT_DIR}/config`).getProperties()
|
||||||
|
const redis = require(`${LIB_DIR}/redis`)({
|
||||||
|
...config.redis,
|
||||||
|
...config.redis.email
|
||||||
|
}, mocks.mockLog())
|
||||||
|
|
||||||
|
const cwd = path.resolve(__dirname, ROOT_DIR)
|
||||||
|
|
||||||
|
const KEYS = {
|
||||||
|
current: 'config',
|
||||||
|
previous: 'config.previous'
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('scripts/email-config:', () => {
|
||||||
|
let current, previous
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
return redis.get(KEYS.current)
|
||||||
|
.then(result => {
|
||||||
|
current = result
|
||||||
|
return redis.get(KEYS.previous)
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
previous = result
|
||||||
|
return redis.del(KEYS.current)
|
||||||
|
})
|
||||||
|
.then(() => redis.del(KEYS.previous))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
return P.resolve()
|
||||||
|
.then(() => {
|
||||||
|
if (current) {
|
||||||
|
return redis.set(KEYS.current, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return redis.del(KEYS.current)
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (previous) {
|
||||||
|
return redis.set(KEYS.previous, previous)
|
||||||
|
}
|
||||||
|
|
||||||
|
return redis.del(KEYS.previous)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('read does not fail', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config read', { cwd })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('write does not fail', () => {
|
||||||
|
return cp.execAsync('echo "{}" | node scripts/email-config write', { cwd })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('write fails if stdin is not valid JSON', () => {
|
||||||
|
return cp.execAsync('echo "wibble" | node scripts/email-config write', { cwd })
|
||||||
|
.then(() => assert(false, 'script should have failed'))
|
||||||
|
.catch(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('revert does not fail', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config revert', { cwd })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('check does not fail', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config check foo@example.com', { cwd })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('check fails without argument', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config check', { cwd })
|
||||||
|
.then(() => assert(false, 'script should have failed'))
|
||||||
|
.catch(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('write config with regex:', () => {
|
||||||
|
let config
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
config = {
|
||||||
|
sendgrid: {
|
||||||
|
regex: 'foo',
|
||||||
|
percentage: 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cp.execAsync(`echo '${JSON.stringify(config)}' | node scripts/email-config write`, { cwd })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('read prints the config to stdout', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config read', { cwd })
|
||||||
|
.then((stdout, stderr) => {
|
||||||
|
assert.equal(stdout, `${JSON.stringify(config, null, ' ')}\n`)
|
||||||
|
assert.equal(stderr, undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('check matching email prints the config to stdout', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config check foo@example.com', { cwd })
|
||||||
|
.then((stdout, stderr) => {
|
||||||
|
assert.equal(stdout, `${JSON.stringify(config, null, ' ')}\n`)
|
||||||
|
assert.equal(stderr, undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('check non-matching email does not print', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config check bar@example.com', { cwd })
|
||||||
|
.then((stdout, stderr) => {
|
||||||
|
assert.equal(stdout, '{}\n')
|
||||||
|
assert.equal(stderr, undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('write config without regex:', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
config.socketlabs = {
|
||||||
|
percentage: 10
|
||||||
|
}
|
||||||
|
return cp.execAsync(`echo '${JSON.stringify(config)}' | node scripts/email-config write`, { cwd })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('read prints the config to stdout', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config read', { cwd })
|
||||||
|
.then((stdout, stderr) => {
|
||||||
|
assert.equal(stdout, `${JSON.stringify(config, null, ' ')}\n`)
|
||||||
|
assert.equal(stderr, undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('check matching email prints the both configs to stdout', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config check foo@example.com', { cwd })
|
||||||
|
.then((stdout, stderr) => {
|
||||||
|
assert.equal(stdout, `${JSON.stringify(config, null, ' ')}\n`)
|
||||||
|
assert.equal(stderr, undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('check non-matching email prints one config to stdout', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config check bar@example.com', { cwd })
|
||||||
|
.then((stdout, stderr) => {
|
||||||
|
const expected = {
|
||||||
|
socketlabs: config.socketlabs
|
||||||
|
}
|
||||||
|
assert.equal(stdout, `${JSON.stringify(expected, null, ' ')}\n`)
|
||||||
|
assert.equal(stderr, undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('revert:', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return cp.execAsync('node scripts/email-config revert', { cwd })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('read prints the previous config to stdout', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config read', { cwd })
|
||||||
|
.then((stdout, stderr) => {
|
||||||
|
const expected = {
|
||||||
|
sendgrid: config.sendgrid
|
||||||
|
}
|
||||||
|
assert.equal(stdout, `${JSON.stringify(expected, null, ' ')}\n`)
|
||||||
|
assert.equal(stderr, undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('check matching email prints the previous config to stdout', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config check foo@example.com', { cwd })
|
||||||
|
.then((stdout, stderr) => {
|
||||||
|
const expected = {
|
||||||
|
sendgrid: config.sendgrid
|
||||||
|
}
|
||||||
|
assert.equal(stdout, `${JSON.stringify(expected, null, ' ')}\n`)
|
||||||
|
assert.equal(stderr, undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('check non-matching email does not print', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config check bar@example.com', { cwd })
|
||||||
|
.then((stdout, stderr) => {
|
||||||
|
assert.equal(stdout, '{}\n')
|
||||||
|
assert.equal(stderr, undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('revert:', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
return cp.execAsync('node scripts/email-config revert', { cwd })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('read does not print', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config read', { cwd })
|
||||||
|
.then((stdout, stderr) => {
|
||||||
|
assert.equal(stdout, '')
|
||||||
|
assert.equal(stderr, undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('check matching email does not print', () => {
|
||||||
|
return cp.execAsync('node scripts/email-config check foo@example.com', { cwd })
|
||||||
|
.then((stdout, stderr) => {
|
||||||
|
assert.equal(stdout, '')
|
||||||
|
assert.equal(stderr, undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Загрузка…
Ссылка в новой задаче