Skip to content

Commit 76d941c

Browse files
authored
Cleaned up ghost/core db (integration) test suite noise (#28909)
no ref Removes the bulk of the error-level log noise from the DB-backed test suites (integration / e2e), where deliberate error paths were logging hundreds of lines per run and burying real failures.
1 parent 1c96f5f commit 76d941c

37 files changed

Lines changed: 520 additions & 59 deletions

ghost/core/core/server/services/email-address/email-address-service.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,19 @@ export class EmailAddressService {
101101
*/
102102
getAddress(preferred: EmailAddresses, options: GetAddressOptions = {useFallbackAddress: false}): EmailAddresses {
103103
if (preferred.replyTo && !this.#isValidEmailAddress(preferred.replyTo.address)) {
104+
// The default from-address itself is allowed to fail generic email validation
105+
// (e.g. noreply@127.0.0.1 on an IP-only install — see the same carve-out in
106+
// validate() below), so don't error-log when the replyTo just happens to match
107+
// it (case-insensitively — members_support_address is user-entered, so a case
108+
// variant of the same address shouldn't defeat the carve-out). This also
109+
// silences a genuinely malformed mail:from, not just the known IP-domain case —
110+
// demote to warn instead of dropping the signal entirely.
111+
if (preferred.replyTo.address.toLowerCase() !== this.defaultFromEmail.address.toLowerCase()) {
112+
logging.error(`[EmailAddresses] Invalid replyTo address: ${preferred.replyTo.address}`);
113+
} else {
114+
logging.warn(`[EmailAddresses] replyTo address ${preferred.replyTo.address} matches the default from-address and failed generic validation; removing it`);
115+
}
104116
// Remove invalid replyTo addresses
105-
logging.error(`[EmailAddresses] Invalid replyTo address: ${preferred.replyTo.address}`);
106117
preferred.replyTo = undefined;
107118
}
108119

@@ -153,8 +164,10 @@ export class EmailAddressService {
153164
return preferred;
154165
}
155166

156-
// Invalid configuration: don't allow to send from this sending domain
157-
logging.error(`[EmailAddresses] Invalid configuration: cannot send emails from ${preferred.from.address} when sending domain is ${this.sendingDomain}`);
167+
// Not an error: this is the expected, handled fallback for any sender address that
168+
// doesn't match the sending domain (staff/member/newsletter senders routinely don't) —
169+
// we fall through to the default from-address below. Warn-level, not error-level.
170+
logging.warn(`[EmailAddresses] Cannot send emails from ${preferred.from.address} when sending domain is ${this.sendingDomain} — falling back to the default from-address`);
158171
}
159172

160173
// Only allow to send from the configured from address

ghost/core/core/server/services/explore-ping/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ module.exports.createService = function createService() {
2525
};
2626

2727
module.exports.init = async function init() {
28+
// The explore ping is a background "phone home" request. It should not run
29+
// in the test environment (cf. the update-check service, which gates on the
30+
// same environments), where there is no explore URL configured.
31+
if (!config.isProductionOrDevelopment()) {
32+
return;
33+
}
34+
2835
const explorePingService = module.exports.createService();
2936

3037
// The final intention is to have this run on a schedule

ghost/core/core/server/services/stripe/service.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const settingsCache = require('../../../shared/settings-cache');
1818
async function configureApi() {
1919
const cfg = getConfig({settingsHelpers, config, urlUtils});
2020
if (cfg) {
21-
cfg.testEnv = process.env.NODE_ENV.startsWith('test');
21+
cfg.testEnv = config.isTestEnv();
2222
await module.exports.configure(cfg);
2323
return true;
2424
}

ghost/core/core/server/services/stripe/stripe-service.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,23 @@ module.exports = class StripeService {
206206
webhookSecret: config.webhookSecret,
207207
webhookHandlerUrl: config.webhookHandlerUrl
208208
});
209-
await this.webhookManager.start();
210209

211210
this.billingPortalManager.configure({
212211
siteUrl: config.siteUrl
213212
});
214-
await this.billingPortalManager.start();
213+
214+
// webhookManager.start() already self-guards: configure() above puts it in
215+
// 'local' mode whenever a webhookSecret is set, which is always true outside
216+
// production (config.js defaults it to DEFAULT_WEBHOOK_SECRET), so start()
217+
// returns immediately without touching Stripe. Only billingPortalManager
218+
// needs an explicit test-env skip — in the test env there is no real Stripe
219+
// to register against, and the mock returns 500 for billing_portal/
220+
// configurations, so this network-registration call only error-logs on
221+
// every boot. Tests never need a registered portal configuration. Skip it
222+
// under test; prod and dev register exactly as before.
223+
await this.webhookManager.start();
224+
if (!config.testEnv) {
225+
await this.billingPortalManager.start();
226+
}
215227
}
216228
};

ghost/core/core/server/services/update-check/index.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11

2-
const _ = require('lodash');
3-
42
const api = require('../../api').endpoints;
53
const config = require('../../../shared/config');
64
const urlUtils = require('../../../shared/url-utils');
@@ -25,10 +23,8 @@ module.exports = async ({
2523
updateCheckUrl = config.get('updateCheck:url')
2624
} = {}) => {
2725
if (!forceUpdate) {
28-
const allowedCheckEnvironments = ['development', 'production'];
29-
30-
// CASE: The check will not happen if your NODE_ENV is not in the allowed defined environments
31-
if (_.indexOf(allowedCheckEnvironments, process.env.NODE_ENV) === -1) {
26+
// CASE: The check will not happen if your env is not in the allowed defined environments
27+
if (!config.isProductionOrDevelopment()) {
3228
return;
3329
}
3430
}

ghost/core/core/server/web/parent/middleware/log-request.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const logging = require('@tryghost/logging');
2+
const config = require('../../../../shared/config');
23

34
/**
45
* @TODO: move this middleware to Framework monorepo?
@@ -15,7 +16,17 @@ module.exports = function logRequest(req, res, next) {
1516
req.userId = req.user ? (req.user.id ? req.user.id : req.user) : null;
1617

1718
if (req.err && req.err.statusCode !== 404) {
18-
logging.error({req: req, res: res, err: req.err});
19+
// 4xx are client errors (validation, auth, rate limit), not server
20+
// faults. Production keeps them at error (they're monitored); the test
21+
// env sets logging:logClientErrorsAsError=false to demote them to warn,
22+
// where every deliberate error-response assertion (expectStatus(4xx))
23+
// would otherwise log a redundant line.
24+
const isClientError = req.err.statusCode >= 400 && req.err.statusCode < 500;
25+
if (isClientError && config.get('logging:logClientErrorsAsError') === false) {
26+
logging.warn({req: req, res: res, err: req.err});
27+
} else {
28+
logging.error({req: req, res: res, err: req.err});
29+
}
1930
} else {
2031
logging.info({req: req, res: res});
2132
}

ghost/core/core/shared/config/defaults.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
},
7373
"logging": {
7474
"level": "info",
75+
"logClientErrorsAsError": true,
7576
"useLocalTime": false,
7677
"rotation": {
7778
"enabled": false,

ghost/core/core/shared/config/env/config.testing-mysql.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
}
1717
},
1818
"logging": {
19-
"level": "error"
19+
"level": "error",
20+
"logClientErrorsAsError": false
2021
},
2122
"adapters": {
2223
"Redis": {

ghost/core/core/shared/config/env/config.testing.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"port": 2369
1313
},
1414
"logging": {
15-
"level": "error"
15+
"level": "error",
16+
"logClientErrorsAsError": false
1617
},
1718
"adapters": {
1819
"Redis": {

ghost/core/core/shared/config/helpers.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,42 @@ const getContentPath = function getContentPath(type) {
9797
}
9898
};
9999

100+
/**
101+
* @callback isTestEnvFn
102+
* @returns {boolean}
103+
*/
104+
const isTestEnv = function isTestEnv() {
105+
return this.get('env').startsWith('test');
106+
};
107+
108+
/**
109+
* env defaults to 'development' when NODE_ENV is unset (see getNodeEnv() in
110+
* ./utils.js), matching ghost.js's own `process.env.NODE_ENV = process.env.NODE_ENV
111+
* || 'development'` at the real CLI entry point — so in any Ghost boot via the
112+
* normal entry point, this and a raw `process.env.NODE_ENV` check agree. They
113+
* only diverge for programmatic embedders that require core modules directly
114+
* without going through ghost.js and never set NODE_ENV themselves — those now
115+
* count as 'development' (e.g. explore-ping/update-check will phone home)
116+
* rather than being silently skipped.
117+
* @callback isProductionOrDevelopmentFn
118+
* @returns {boolean}
119+
*/
120+
const isProductionOrDevelopment = function isProductionOrDevelopment() {
121+
return ['development', 'production'].includes(this.get('env'));
122+
};
123+
100124
/**
101125
* @typedef ConfigHelpers
102126
* @property {isPrivacyDisabledFn} isPrivacyDisabled
103127
* @property {getContentPathFn} getContentPath
128+
* @property {isTestEnvFn} isTestEnv
129+
* @property {isProductionOrDevelopmentFn} isProductionOrDevelopment
104130
*/
105131
module.exports.bindAll = (nconf) => {
106132
nconf.isPrivacyDisabled = isPrivacyDisabled.bind(nconf);
107133
nconf.getContentPath = getContentPath.bind(nconf);
108134
nconf.getBackendMountPath = getBackendMountPath.bind(nconf);
109135
nconf.getFrontendMountPath = getFrontendMountPath.bind(nconf);
136+
nconf.isTestEnv = isTestEnv.bind(nconf);
137+
nconf.isProductionOrDevelopment = isProductionOrDevelopment.bind(nconf);
110138
};

0 commit comments

Comments
 (0)