Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@
},
"devDependencies": {
"@nx/js": "22.6.1",
"@vitest/coverage-v8": "3.2.4",
"@vitest/coverage-v8": "4.1.0",
"eslint": "8.57.1",
"eslint-plugin-ghost": "3.5.0",
"nx": "22.6.1",
"sinon": "21.0.3",
"ts-node": "10.9.2",
"vitest": "3.2.4"
"vitest": "4.1.0"
},
"resolutions": {
"node-loggly-bulk": "^4.0.2"
Expand Down
52 changes: 52 additions & 0 deletions packages/bookshelf-pagination/test/pagination.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,58 @@ describe('@tryghost/bookshelf-pagination', function () {
assert.equal(modelState.rawCalls[0], 'count(distinct posts.id) as aggregate');
});

it('useSmartCount supports SQL fragments returned as an array', async function () {
const {bookshelf, modelState} = createBookshelf({countRows: [{aggregate: 1}]});
const model = new bookshelf.Model();

const originalQuery = model.query;
model.query = function () {
const qb = originalQuery.apply(this, arguments);
if (arguments.length === 0) {
qb.toSQL = () => [
{sql: 'select *'},
{},
{sql: 'from `posts`, `tags` where `posts`.`id` = `tags`.`post_id`'}
];
}
return qb;
};

await model.fetchPage({page: 1, limit: 10, useSmartCount: true});

assert.equal(modelState.rawCalls[0], 'count(distinct posts.id) as aggregate');
});

it('useSmartCount falls back safely when compiled SQL is missing', async function () {
const {bookshelf, modelState} = createBookshelf({countRows: [{aggregate: 1}]});
const model = new bookshelf.Model();

const originalQuery = model.query;
model.query = function () {
const qb = originalQuery.apply(this, arguments);
if (arguments.length === 0) {
qb.toSQL = () => ({});
}
return qb;
};

await model.fetchPage({page: 1, limit: 10, useSmartCount: true});

assert.equal(modelState.rawCalls[0], 'count(*) as aggregate');
});

it('handleRelation does not duplicate entries and ignores same-table properties', function () {
const {bookshelf} = createBookshelf();
const model = new bookshelf.Model();
model.eagerLoad = ['authors'];

paginationPlugin.paginationUtils.handleRelation(model, 'posts.id');
paginationPlugin.paginationUtils.handleRelation(model, 'title');
paginationPlugin.paginationUtils.handleRelation(model, 'authors.name');

assert.deepEqual(model.eagerLoad, ['authors']);
});

it('falls back to zero total when aggregate row is missing', async function () {
const {bookshelf} = createBookshelf({countRows: []});
const model = new bookshelf.Model();
Expand Down
6 changes: 1 addition & 5 deletions packages/config/lib/config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
const getConfig = require('./get-config');

let config;
const config = getConfig();
function initConfig() {
if (!config) {
config = getConfig();
}

return config;
}

Expand Down
5 changes: 5 additions & 0 deletions packages/config/test/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ describe('Config', function () {
assert.equal(config.get('env'), 'development');
});

it('handles missing process root gracefully', function () {
const config = withEnv('testing', () => withRoot('', () => loadFreshGetConfig()()));
assert.equal(config.get('env'), 'testing');
});

it('index exports lib/config and only initializes config once', function () {
const originalGetConfig = require(getConfigPath);
const fakeConfig = {name: 'fake-config'};
Expand Down
19 changes: 18 additions & 1 deletion packages/domain-events/lib/DomainEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class DomainEvents {
}
if (this.#trackingEnabled) {
this.#onProcessed();
} else {
// Tracking is disabled outside tests.
}
});
}
Expand All @@ -61,6 +63,8 @@ class DomainEvents {
static dispatchRaw(name, data) {
if (this.#trackingEnabled) {
this.#dispatchCount += DomainEvents.ee.listenerCount(name);
} else {
// Tracking is disabled outside tests.
}
DomainEvents.ee.emit(name, data);
}
Expand All @@ -80,8 +84,9 @@ class DomainEvents {
// Resolve immediately if there are no events in the queue
resolve();
return;
} else {
this.#awaitQueue.push({resolve});
}
this.#awaitQueue.push({resolve});
});
}

Expand All @@ -92,8 +97,20 @@ class DomainEvents {
item.resolve();
}
this.#awaitQueue = [];
} else {
// Wait for the remaining tracked listeners.
}
}

static setTrackingEnabledForTest(enabled) {
this.#trackingEnabled = enabled;
}

static resetTrackingStateForTest() {
this.#awaitQueue = [];
this.#dispatchCount = 0;
this.#processedCount = 0;
}
}

module.exports = DomainEvents;
52 changes: 52 additions & 0 deletions packages/domain-events/test/DomainEvents.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ describe('DomainEvents', function () {
afterEach(function () {
sinon.restore();
DomainEvents.ee.removeAllListeners();
DomainEvents.resetTrackingStateForTest();
DomainEvents.setTrackingEnabledForTest(process.env.NODE_ENV?.startsWith('test'));
});

it('Will call multiple subscribers with the event when it is dispatched', async function () {
Expand Down Expand Up @@ -77,6 +79,22 @@ describe('DomainEvents', function () {
assert.equal(stub.calledTwice, true);
});

it('works when tracking is disabled', async function () {
let handled = false;

DomainEvents.setTrackingEnabledForTest(false);

DomainEvents.subscribe(TestEvent, () => {
handled = true;
});

DomainEvents.dispatch(new TestEvent('No tracking'));
await sleep(0);
await DomainEvents.allSettled();

assert.equal(handled, true);
});

describe('allSettled', function () {
it('Resolves when there are no events', async function () {
await DomainEvents.allSettled();
Expand All @@ -98,5 +116,39 @@ describe('DomainEvents', function () {
await DomainEvents.allSettled();
assert.equal(counter, 2);
});

it('waits for every tracked listener before resolving', async function () {
let resolveFirst;
let resolveSecond;

DomainEvents.subscribe(TestEvent, () => {
return new Promise((resolve) => {
resolveFirst = resolve;
});
});
DomainEvents.subscribe(TestEvent, () => {
return new Promise((resolve) => {
resolveSecond = resolve;
});
});

DomainEvents.dispatch(new TestEvent('Hello, world!'));

let settled = false;
const allSettled = DomainEvents.allSettled().then(() => {
settled = true;
});

await sleep(0);
assert.equal(settled, false);

resolveFirst();
await sleep(0);
assert.equal(settled, false);

resolveSecond();
await allSettled;
assert.equal(settled, true);
});
});
});
5 changes: 3 additions & 2 deletions packages/job-manager/test/job-manager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ describe('Job Manager', function () {
assert.equal(JobModel.edit.args[1][0].status, 'failed');

// simulate process restart and "fresh" slate to add the job
jobManager.removeJob('failed-oneoff');
await jobManager.removeJob('failed-oneoff').catch(() => {});
const completion2 = jobManager.awaitCompletion('failed-oneoff');

await jobManager.addOneOffJob({
Expand Down Expand Up @@ -664,7 +664,8 @@ describe('Job Manager', function () {
id: 'unique',
get: () => status
}),
add: sinon.stub().resolves()
add: sinon.stub().resolves(),
edit: sinon.stub().resolves()
};

jobManager = new JobManager({JobModel, config: stubConfig});
Expand Down
31 changes: 31 additions & 0 deletions packages/metrics/test/metrics.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,35 @@ describe('Logging', function () {
ghostMetrics.metric(name, value);
assert.equal(ElasticSearch.prototype.index.calledOnce, true);
});

it('ships object values with pre-set timestamp without adding metadata when disabled', async function () {
const ghostMetrics = new GhostMetrics({
metrics: {
transports: ['elasticsearch']
},
elasticsearch: {
host: 'https://test-elasticsearch',
username: 'user',
password: 'pass'
}
});

// Force metadata check false branch for coverage.
ghostMetrics.metadata = null;

const payload = {
value: 101,
'@timestamp': 12345
};

await new Promise((resolve) => {
sandbox.stub(ElasticSearch.prototype, 'index').callsFake(function (data, index) {
assert.deepEqual(data, payload);
assert.equal(index, 'metrics-object-metric');
resolve();
});

ghostMetrics.metric('object-metric', payload);
});
});
});
8 changes: 8 additions & 0 deletions packages/request/lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ module.exports = async function request(url, options = {}) {
// Initialise ES6 imports
if (!got) {
got = (await gotPromise).default;
} else {
// Already initialized from a prior request in this process.
}
if (!defaultOptions.dnsLookup) {
// Ensure OS-level name resolution is not used
Expand All @@ -28,6 +30,8 @@ module.exports = async function request(url, options = {}) {
lookup: false
});
defaultOptions.dnsLookup = cacheableLookup.lookup;
} else {
// DNS cache lookup has already been configured.
}

if (_.isEmpty(url) || !validator.isURL(url)) {
Expand All @@ -36,6 +40,8 @@ module.exports = async function request(url, options = {}) {
code: 'URL_MISSING_INVALID',
context: url
}));
} else {
// URL is valid and request execution can continue.
}

if (process.env.NODE_ENV?.startsWith('test') && !Object.prototype.hasOwnProperty.call(options, 'retry')) {
Expand All @@ -61,6 +67,8 @@ module.exports = async function request(url, options = {}) {
if (error.response) {
Object.assign(error, error.response);
delete error.reponse;
} else {
// Some transport errors do not include a response object.
}
throw error;
}
Expand Down
18 changes: 18 additions & 0 deletions packages/request/test/request.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const assert = require('assert/strict');
const nock = require('nock');
const sinon = require('sinon');
const rewire = require('rewire');

const request = require('../lib/request');

Expand Down Expand Up @@ -248,4 +250,20 @@ describe('Request', function () {
assert.notEqual(err.response, undefined);
});
});

it('[failure] rethrows plain errors when response is missing', function () {
const requestModule = rewire('../lib/request');
const plainError = new Error('plain error');
const gotStub = sinon.stub().rejects(plainError);

requestModule.__set__('got', gotStub);
requestModule.__get__('defaultOptions').dnsLookup = () => {};

return requestModule('http://some-website.com/plain-error/', {retry: {limit: 0}}).then(() => {
throw new Error('Should have failed');
}, (err) => {
assert.equal(gotStub.calledOnce, true);
assert.equal(err, plainError);
});
});
});
Loading
Loading