Skip to content

Commit 497b80e

Browse files
test: add integration tests for bug and better integration tests for transform cursor logic
1 parent 3683d0b commit 497b80e

File tree

1 file changed

+222
-73
lines changed

1 file changed

+222
-73
lines changed
Lines changed: 222 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,263 @@
11
import { expect } from 'chai';
2+
import { once } from 'events';
3+
import * as sinon from 'sinon';
24
import { inspect } from 'util';
35

4-
import { type Collection, MongoAPIError, type MongoClient } from '../../mongodb';
5-
6-
const falseyValues = [0, 0n, NaN, '', false, undefined];
6+
import { type Collection, type FindCursor, MongoAPIError, type MongoClient } from '../../mongodb';
77

88
describe('class AbstractCursor', function () {
9-
let client: MongoClient;
9+
describe('regression tests NODE-5372', function () {
10+
let client: MongoClient;
11+
let collection: Collection;
12+
const docs = [{ count: 0 }, { count: 10 }];
13+
beforeEach(async function () {
14+
client = this.configuration.newClient();
1015

11-
let collection: Collection;
12-
beforeEach(async function () {
13-
client = this.configuration.newClient();
16+
collection = client.db('abstract_cursor_integration').collection('test');
1417

15-
collection = client.db('abstract_cursor_integration').collection('test');
18+
await collection.insertMany(docs);
19+
});
1620

17-
await collection.insertMany(Array.from({ length: 5 }, (_, index) => ({ index })));
18-
});
21+
afterEach(async function () {
22+
await collection.deleteMany({});
23+
await client.close();
24+
});
1925

20-
afterEach(async function () {
21-
await collection.deleteMany({});
22-
await client.close();
23-
});
26+
it('cursors can be iterated with hasNext+next', async function () {
27+
const cursor = collection
28+
// sort ensures that the docs in the cursor are in the same order as the docs inserted
29+
.find({}, { sort: { count: 1 } })
30+
.map(doc => ({ ...doc, count: doc.count + 1 }));
2431

25-
context('toArray() with custom transforms', function () {
26-
for (const value of falseyValues) {
27-
it(`supports mapping to falsey value '${inspect(value)}'`, async function () {
28-
const cursor = collection.find();
29-
cursor.map(() => value);
32+
for (let count = 0; await cursor.hasNext(); count++) {
33+
const received = await cursor.next();
34+
const actual = docs[count];
3035

31-
const result = await cursor.toArray();
36+
expect(received.count).to.equal(actual.count + 1);
37+
}
38+
});
39+
});
3240

33-
const expected = Array.from({ length: 5 }, () => value);
34-
expect(result).to.deep.equal(expected);
35-
});
36-
}
41+
describe('cursor iteration APIs', function () {
42+
let client: MongoClient;
43+
let collection: Collection;
44+
const transformSpy = sinon.spy(doc => ({ ...doc, name: doc.name.toUpperCase() }));
45+
beforeEach(async function () {
46+
client = this.configuration.newClient();
47+
48+
collection = client.db('abstract_cursor_integration').collection('test');
3749

38-
it('throws when mapping to `null` and cleans up cursor', async function () {
39-
const cursor = collection.find();
40-
cursor.map(() => null);
50+
await collection.insertMany([{ name: 'john doe' }]);
51+
});
4152

42-
const error = await cursor.toArray().catch(e => e);
53+
afterEach(async function () {
54+
transformSpy.resetHistory();
4355

44-
expect(error).be.instanceOf(MongoAPIError);
45-
expect(cursor.closed).to.be.true;
56+
await collection.deleteMany({});
57+
await client.close();
4658
});
47-
});
4859

49-
context('Symbol.asyncIterator() with custom transforms', function () {
50-
for (const value of falseyValues) {
51-
it(`supports mapping to falsey value '${inspect(value)}'`, async function () {
52-
const cursor = collection.find();
53-
cursor.map(() => value);
60+
describe('tryNext()', function () {
61+
context('when there is a transform on the cursor', function () {
62+
it('does not transform any documents', async function () {
63+
const cursor = collection.find().map(transformSpy);
5464

55-
let count = 0;
65+
await cursor.hasNext();
66+
expect(transformSpy.called).to.be.false;
67+
});
68+
});
69+
});
5670

57-
for await (const document of cursor) {
58-
expect(document).to.deep.equal(value);
59-
count++;
71+
const operations: ReadonlyArray<readonly [string, (arg0: FindCursor) => Promise<unknown>]> = [
72+
[
73+
'tryNext',
74+
(cursor: FindCursor) => {
75+
return cursor.tryNext();
76+
}
77+
],
78+
['next', (cursor: FindCursor) => cursor.next()],
79+
[
80+
'Symbol.asyncIterator().next',
81+
async (cursor: FindCursor) => {
82+
const iterator = cursor[Symbol.asyncIterator]();
83+
const doc = await iterator.next();
84+
return doc.value;
6085
}
86+
]
87+
] as const;
6188

62-
expect(count).to.equal(5);
89+
context('when there is a transform on the cursor', function () {
90+
for (const [method, func] of operations) {
91+
it(`${method}() calls the cursor transform when iterated`, async () => {
92+
const cursor = collection.find().map(transformSpy);
93+
94+
const doc = await func(cursor);
95+
expect(transformSpy).to.have.been.calledOnce;
96+
expect(doc.name).to.equal('JOHN DOE');
97+
});
98+
99+
// skipped because these tests fail after throwing uncaught exceptions
100+
it(`when the transform throws, ${method}() propagates the error to the user`, async () => {
101+
const cursor = collection.find().map(() => {
102+
throw new Error('error thrown in transform');
103+
});
104+
105+
const error = await func(cursor).catch(e => e);
106+
expect(error)
107+
.to.be.instanceOf(Error)
108+
.to.match(/error thrown in transform/);
109+
});
110+
}
111+
112+
it('Cursor.stream() calls the cursor transform when iterated', async function () {
113+
const cursor = collection.find().map(transformSpy).stream();
114+
115+
const [doc] = await once(cursor, 'data');
116+
expect(transformSpy).to.have.been.calledOnce;
117+
expect(doc.name).to.equal('JOHN DOE');
63118
});
64-
}
65119

66-
it('throws when mapping to `null` and cleans up cursor', async function () {
67-
const cursor = collection.find();
68-
cursor.map(() => null);
120+
// skipped because these tests fail after throwing uncaught exceptions
121+
it(`when the transform throws, Cursor.stream() propagates the error to the user`, async () => {
122+
const cursor = collection
123+
.find()
124+
.map(() => {
125+
throw new Error('error thrown in transform');
126+
})
127+
.stream();
69128

70-
try {
71-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
72-
for await (const document of cursor) {
73-
expect.fail('Expected error to be thrown');
74-
}
75-
} catch (error) {
76-
expect(error).to.be.instanceOf(MongoAPIError);
77-
expect(cursor.closed).to.be.true;
129+
const error = await once(cursor, 'data').catch(e => e);
130+
expect(error)
131+
.to.be.instanceOf(Error)
132+
.to.match(/error thrown in transform/);
133+
});
134+
});
135+
136+
context('when there is not a transform on the cursor', function () {
137+
for (const [method, func] of operations) {
138+
it(`${method}() returns the documents, unmodified`, async () => {
139+
const cursor = collection.find();
140+
141+
const doc = await func(cursor);
142+
expect(doc.name).to.equal('john doe');
143+
});
78144
}
145+
146+
it('Cursor.stream() returns the documents, unmodified', async function () {
147+
const cursor = collection.find().stream();
148+
149+
const [doc] = await once(cursor, 'data');
150+
expect(doc.name).to.equal('john doe');
151+
});
79152
});
80153
});
81154

82-
context('forEach() with custom transforms', function () {
83-
for (const value of falseyValues) {
84-
it(`supports mapping to falsey value '${inspect(value)}'`, async function () {
155+
describe('custom transforms with falsy values', function () {
156+
let client: MongoClient;
157+
const falseyValues = [0, 0n, NaN, '', false, undefined];
158+
159+
let collection: Collection;
160+
beforeEach(async function () {
161+
client = this.configuration.newClient();
162+
163+
collection = client.db('abstract_cursor_integration').collection('test');
164+
165+
await collection.insertMany(Array.from({ length: 5 }, (_, index) => ({ index })));
166+
});
167+
168+
afterEach(async function () {
169+
await collection.deleteMany({});
170+
await client.close();
171+
});
172+
173+
context('toArray() with custom transforms', function () {
174+
for (const value of falseyValues) {
175+
it(`supports mapping to falsey value '${inspect(value)}'`, async function () {
176+
const cursor = collection.find();
177+
cursor.map(() => value);
178+
179+
const result = await cursor.toArray();
180+
181+
const expected = Array.from({ length: 5 }, () => value);
182+
expect(result).to.deep.equal(expected);
183+
});
184+
}
185+
186+
it('throws when mapping to `null` and cleans up cursor', async function () {
85187
const cursor = collection.find();
86-
cursor.map(() => value);
188+
cursor.map(() => null);
87189

88-
let count = 0;
190+
const error = await cursor.toArray().catch(e => e);
89191

90-
function transform(value) {
91-
expect(value).to.deep.equal(value);
92-
count++;
93-
}
192+
expect(error).be.instanceOf(MongoAPIError);
193+
expect(cursor.closed).to.be.true;
194+
});
195+
});
196+
197+
context('Symbol.asyncIterator() with custom transforms', function () {
198+
for (const value of falseyValues) {
199+
it(`supports mapping to falsey value '${inspect(value)}'`, async function () {
200+
const cursor = collection.find();
201+
cursor.map(() => value);
202+
203+
let count = 0;
94204

95-
await cursor.forEach(transform);
205+
for await (const document of cursor) {
206+
expect(document).to.deep.equal(value);
207+
count++;
208+
}
96209

97-
expect(count).to.equal(5);
210+
expect(count).to.equal(5);
211+
});
212+
}
213+
214+
it('throws when mapping to `null` and cleans up cursor', async function () {
215+
const cursor = collection.find();
216+
cursor.map(() => null);
217+
218+
try {
219+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
220+
for await (const document of cursor) {
221+
expect.fail('Expected error to be thrown');
222+
}
223+
} catch (error) {
224+
expect(error).to.be.instanceOf(MongoAPIError);
225+
expect(cursor.closed).to.be.true;
226+
}
98227
});
99-
}
228+
});
229+
230+
context('forEach() with custom transforms', function () {
231+
for (const value of falseyValues) {
232+
it(`supports mapping to falsey value '${inspect(value)}'`, async function () {
233+
const cursor = collection.find();
234+
cursor.map(() => value);
235+
236+
let count = 0;
100237

101-
it('throws when mapping to `null` and cleans up cursor', async function () {
102-
const cursor = collection.find();
103-
cursor.map(() => null);
238+
function transform(value) {
239+
expect(value).to.deep.equal(value);
240+
count++;
241+
}
104242

105-
function iterator() {
106-
expect.fail('Expected no documents from cursor, received at least one.');
243+
await cursor.forEach(transform);
244+
245+
expect(count).to.equal(5);
246+
});
107247
}
108248

109-
const error = await cursor.forEach(iterator).catch(e => e);
110-
expect(error).to.be.instanceOf(MongoAPIError);
111-
expect(cursor.closed).to.be.true;
249+
it('throws when mapping to `null` and cleans up cursor', async function () {
250+
const cursor = collection.find();
251+
cursor.map(() => null);
252+
253+
function iterator() {
254+
expect.fail('Expected no documents from cursor, received at least one.');
255+
}
256+
257+
const error = await cursor.forEach(iterator).catch(e => e);
258+
expect(error).to.be.instanceOf(MongoAPIError);
259+
expect(cursor.closed).to.be.true;
260+
});
112261
});
113262
});
114263
});

0 commit comments

Comments
 (0)