Skip to content

Commit 03fa0c5

Browse files
committed
test(NODE-4520): optional callback async wrappers
1 parent a539ec3 commit 03fa0c5

File tree

3 files changed

+245
-1
lines changed

3 files changed

+245
-1
lines changed

etc/print_methods.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
3+
const mongodbLegacy = require('..');
4+
5+
const OVERRIDDEN_CLASSES = [
6+
'Admin',
7+
'FindCursor',
8+
'ListCollectionsCursor',
9+
'ListIndexesCursor',
10+
'AggregationCursor',
11+
'ChangeStream',
12+
'Collection',
13+
'Db',
14+
'GridFSBucket',
15+
'MongoClient'
16+
];
17+
18+
const METHODS_MODIFY_PROMISE = new Set([
19+
'Collection.rename',
20+
'Db.createCollection',
21+
'Db.collections',
22+
'MongoClient.connect'
23+
]);
24+
25+
const OVERRIDDEN_CLASSES_TO_CB_METHODS = new Map(
26+
OVERRIDDEN_CLASSES.map(clsName => [
27+
clsName,
28+
Object.getOwnPropertyNames(mongodbLegacy[clsName].prototype)
29+
.filter(propName => propName !== 'constructor')
30+
.filter(propName =>
31+
mongodbLegacy[clsName].prototype[propName].toString().includes('callback) {')
32+
)
33+
.filter(propName => !METHODS_MODIFY_PROMISE.has(`${clsName}.${propName}`))
34+
])
35+
);
36+
37+
// eslint-disable-next-line no-console
38+
console.dir(Object.fromEntries(OVERRIDDEN_CLASSES_TO_CB_METHODS.entries()));

src/legacy_wrappers/session.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ module.exports.makeLegacyClientSession = function (baseClass) {
2727
}
2828

2929
withTransaction(executeWithTransaction, options) {
30-
super.withTransaction(session => executeWithTransaction(session[toLegacy]()), options);
30+
return super.withTransaction(session => executeWithTransaction(session[toLegacy]()), options);
3131
}
3232
}
3333

test/unit/maybe_callback.test.js

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
// @ts-nocheck
2+
'use strict';
3+
4+
const sinon = require('sinon');
5+
const { expect } = require('chai');
6+
7+
const mongodbDriver = require('mongodb');
8+
const { MongoDBNamespace } = require('mongodb/lib/utils');
9+
const mongodbLegacy = require('../..');
10+
const { asyncApi } = require('../tools/api');
11+
12+
const classesToMethods = new Map(
13+
asyncApi.map((api, _, array) => [
14+
api.className,
15+
new Set(array.filter(v => v.className === api.className))
16+
])
17+
);
18+
19+
const iLoveJs = 'mongodb://iLoveJavascript';
20+
const client = new mongodbLegacy.MongoClient(iLoveJs);
21+
const db = new mongodbLegacy.Db(client, 'animals');
22+
const collection = new mongodbLegacy.Collection(db, 'pets');
23+
const namespace = MongoDBNamespace.fromString('animals.pets');
24+
25+
const OVERRIDDEN_CLASSES_GETTER = new Map([
26+
['Admin', () => new mongodbLegacy.Admin(db)],
27+
['FindCursor', () => new mongodbLegacy.FindCursor(client, namespace)],
28+
['ListCollectionsCursor', () => new mongodbLegacy.ListCollectionsCursor(db, {})],
29+
['ListIndexesCursor', () => new mongodbLegacy.ListIndexesCursor(collection)],
30+
['AggregationCursor', () => new mongodbLegacy.AggregationCursor(client, namespace)],
31+
['ChangeStream', () => new mongodbLegacy.ChangeStream(client)],
32+
['GridFSBucket', () => new mongodbLegacy.GridFSBucket(db)],
33+
['Collection', () => new mongodbLegacy.Collection(db, 'pets')],
34+
['Db', () => new mongodbLegacy.Db(client, 'animals')],
35+
['MongoClient', () => new mongodbLegacy.MongoClient(iLoveJs)]
36+
]);
37+
38+
function* generateTests() {
39+
for (const [className, getInstance] of OVERRIDDEN_CLASSES_GETTER) {
40+
for (const { method } of classesToMethods.get(className) ?? []) {
41+
const apiName = `${className}.${method}`;
42+
const instance = getInstance();
43+
yield {
44+
className,
45+
method,
46+
instance,
47+
apiName,
48+
makeStub: superPromise => {
49+
return sinon.stub(mongodbDriver[className].prototype, method).returns(superPromise);
50+
}
51+
};
52+
}
53+
}
54+
}
55+
56+
describe('Maybe Callback', () => {
57+
afterEach(() => {
58+
sinon.restore();
59+
});
60+
61+
for (const { apiName, instance, method, makeStub } of generateTests()) {
62+
context(`${apiName}()`, () => {
63+
it(`returns resolved promise`, async () => {
64+
// should have a message property to make equality checking consistent
65+
const superPromise = Promise.resolve({ message: 'success!' });
66+
makeStub(superPromise);
67+
68+
expect(instance).to.have.property(method).that.is.a('function');
69+
70+
const functionLength = instance[method].length;
71+
const args = Array.from({ length: functionLength - 1 }, (_, i) => i);
72+
const actualReturnValue = instance[method](...args);
73+
74+
// should return the same promise the driver returns
75+
expect(actualReturnValue).to.equal(superPromise);
76+
77+
// should have a message property to make equality checking consistent
78+
await superPromise;
79+
const result = await actualReturnValue.catch(error => error);
80+
81+
expect(result).to.have.property('message', 'success!');
82+
83+
const stubbedMethod = Object.getPrototypeOf(Object.getPrototypeOf(instance))[method];
84+
expect(stubbedMethod).to.have.been.calledOnceWithExactly(...args);
85+
});
86+
87+
it('returns rejected promise', async () => {
88+
const superPromise = Promise.reject(new Error('error!'));
89+
makeStub(superPromise);
90+
91+
expect(instance).to.have.property(method).that.is.a('function');
92+
93+
const functionLength = instance[method].length;
94+
const args = Array.from({ length: functionLength - 1 }, (_, i) => i);
95+
const actualReturnValue = instance[method](...args);
96+
97+
// should return the same promise the driver returns
98+
expect(actualReturnValue).to.equal(superPromise);
99+
100+
// awaiting triggers the callback to be called
101+
await superPromise.catch(error => error);
102+
const result = await actualReturnValue.catch(error => error);
103+
104+
expect(result).to.have.property('message', 'error!');
105+
106+
const stubbedMethod = Object.getPrototypeOf(Object.getPrototypeOf(instance))[method];
107+
expect(stubbedMethod).to.have.been.calledOnceWithExactly(...args);
108+
});
109+
110+
it(`returns void and uses callback(_, result)`, async () => {
111+
const superPromise = Promise.resolve({ message: 'success!' });
112+
makeStub(superPromise);
113+
114+
expect(instance).to.have.property(method).that.is.a('function');
115+
116+
const callback = sinon.spy();
117+
118+
const functionLength = instance[method].length;
119+
const args = Array.from({ length: functionLength }, (_, i) => i);
120+
args[functionLength - 1] = callback;
121+
const actualReturnValue = instance[method](...args);
122+
123+
expect(actualReturnValue).to.be.undefined;
124+
125+
const returnValue = await superPromise.catch(error => error);
126+
expect(callback).to.have.been.calledOnce;
127+
const expectedArgs = callback.args[0];
128+
expect(expectedArgs).to.have.property('0', undefined);
129+
expect(expectedArgs).to.have.nested.property('[1].message', returnValue.message);
130+
131+
const stubbedMethod = Object.getPrototypeOf(Object.getPrototypeOf(instance))[method];
132+
expect(stubbedMethod).to.have.been.calledOnceWithExactly(
133+
...args.slice(0, functionLength - 1)
134+
);
135+
});
136+
137+
it(`returns void and uses callback(error)`, async () => {
138+
const superPromise = Promise.reject(new Error('error!'));
139+
makeStub(superPromise);
140+
141+
expect(instance).to.have.property(method).that.is.a('function');
142+
143+
const callback = sinon.spy();
144+
145+
const functionLength = instance[method].length;
146+
const args = Array.from({ length: functionLength }, (_, i) => i);
147+
args[functionLength - 1] = callback;
148+
const actualReturnValue = instance[method](...args);
149+
150+
expect(actualReturnValue).to.be.undefined;
151+
152+
const returnValue = await superPromise.catch(error => error);
153+
expect(callback).to.have.been.calledOnce;
154+
const expectedArgs = callback.args[0];
155+
expect(expectedArgs).to.have.nested.property('[0].message', returnValue.message);
156+
157+
const stubbedMethod = Object.getPrototypeOf(Object.getPrototypeOf(instance))[method];
158+
expect(stubbedMethod).to.have.been.calledOnceWithExactly(
159+
...args.slice(0, functionLength - 1)
160+
);
161+
});
162+
});
163+
}
164+
165+
it('calling static MongoClient.connect() returns promise', async () => {
166+
const returnValue = Promise.resolve(new mongodbDriver.MongoClient(iLoveJs));
167+
sinon.stub(mongodbDriver.MongoClient, 'connect').returns(returnValue);
168+
const actualReturnValue = mongodbLegacy.MongoClient.connect(iLoveJs);
169+
expect(await actualReturnValue).to.be.instanceOf(mongodbLegacy.MongoClient);
170+
});
171+
172+
it('calling Collection.rename() returns promise', async () => {
173+
const returnValue = Promise.resolve(new mongodbDriver.Collection(db, 'a'));
174+
sinon.stub(mongodbDriver.Collection.prototype, 'rename').returns(returnValue);
175+
expect(collection).to.have.property('rename').that.is.a('function');
176+
const actualReturnValue = collection.rename('a');
177+
expect(await actualReturnValue).to.be.instanceOf(mongodbLegacy.Collection);
178+
});
179+
180+
it('calling Db.createCollection() returns promise', async () => {
181+
const returnValue = Promise.resolve(new mongodbDriver.Collection(db, 'a'));
182+
sinon.stub(mongodbDriver.Db.prototype, 'createCollection').returns(returnValue);
183+
expect(db).to.have.property('createCollection').that.is.a('function');
184+
const actualReturnValue = db.createCollection('a');
185+
expect(await actualReturnValue).to.be.instanceOf(mongodbLegacy.Collection);
186+
});
187+
188+
it('calling Db.collections() returns promise', async () => {
189+
const returnValue = Promise.resolve([
190+
new mongodbDriver.Collection(db, 'a'),
191+
new mongodbDriver.Collection(db, 'b')
192+
]);
193+
sinon.stub(mongodbDriver.Db.prototype, 'collections').returns(returnValue);
194+
expect(db).to.have.property('collections').that.is.a('function');
195+
const actualReturnValue = db.collections();
196+
expect(await actualReturnValue).to.be.an('array');
197+
});
198+
199+
it('calling MongoClient.connect() returns promise', async () => {
200+
const returnValue = Promise.resolve(new mongodbDriver.MongoClient(iLoveJs));
201+
sinon.stub(mongodbDriver.MongoClient.prototype, 'connect').returns(returnValue);
202+
expect(client).to.have.property('connect').that.is.a('function');
203+
const actualReturnValue = client.connect();
204+
expect(await actualReturnValue).to.be.instanceOf(mongodbLegacy.MongoClient);
205+
});
206+
});

0 commit comments

Comments
 (0)