Skip to content

Commit 50ad8e6

Browse files
authored
test(mongodb): verify user schema behaviors under connection state 2 (JhaSourav07#2055)
Fixes JhaSourav07#1425 ## Description Added comprehensive unit tests inside models/User.test.ts to verify User model and schema behaviors under database connection state 2 (connecting), ensuring incoming queries interact properly with internal buffering mechanisms. ## Pillar - [x] 🛠️ Other (Bug fix, refactoring, docs, testing) ## Checklist - [x] I have read the CONTRIBUTING.md file. - [x] My commits follow the Conventional Commits format. - [x] I have made sure that I have only one commit to merge in this PR.
2 parents e572add + 6d1bb83 commit 50ad8e6

1 file changed

Lines changed: 98 additions & 35 deletions

File tree

models/User.test.ts

Lines changed: 98 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import mongoose from 'mongoose';
2-
import { describe, it, expect } from 'vitest';
2+
import { describe, it, expect, afterEach, vi } from 'vitest';
33
import { User } from './User';
44

55
describe('User Model', () => {
6-
it('is compiled properly and exposed', () => {
6+
it('is compiled properly and exposed', (): void => {
77
expect(User).toBeDefined();
88
expect(User.modelName).toBe('User');
99
});
1010

1111
describe('username schema constraints', () => {
12-
it('has lowercase: true on username path', () => {
12+
it('has lowercase: true on username path', (): void => {
1313
const usernamePath = User.schema.path('username') as mongoose.SchemaType & {
1414
options: Record<string, unknown>;
1515
};
1616
expect(usernamePath.options.lowercase).toBe(true);
1717
});
1818

1919
describe('createdAt schema', () => {
20-
it('uses a callable default that returns a timestamp', () => {
20+
it('uses a callable default that returns a timestamp', (): void => {
2121
const createdAtPath = User.schema.path('createdAt') as mongoose.SchemaType & {
2222
options: { default?: unknown };
2323
};
@@ -29,7 +29,7 @@ describe('User Model', () => {
2929
expect(Number.isFinite(result)).toBe(true);
3030
});
3131

32-
it('has a defined defaultValue that is Date.now or returns a Date', () => {
32+
it('has a defined defaultValue that is Date.now or returns a Date', (): void => {
3333
const createdAtPath = User.schema.path('createdAt') as mongoose.SchemaType & {
3434
defaultValue?: unknown;
3535
options: { default?: unknown };
@@ -47,21 +47,21 @@ describe('User Model', () => {
4747
});
4848
});
4949

50-
it('has trim: true on username path', () => {
50+
it('has trim: true on username path', (): void => {
5151
const usernamePath = User.schema.path('username') as mongoose.SchemaType & {
5252
options: Record<string, unknown>;
5353
};
5454
expect(usernamePath.options.trim).toBe(true);
5555
});
5656

57-
it('has unique: true on username path', () => {
57+
it('has unique: true on username path', (): void => {
5858
const usernamePath = User.schema.path('username') as mongoose.SchemaType & {
5959
options: Record<string, unknown>;
6060
};
6161
expect(usernamePath.options.unique).toBe(true);
6262
});
6363

64-
it('has required: true on username path', () => {
64+
it('has required: true on username path', (): void => {
6565
const usernamePath = User.schema.path('username') as mongoose.SchemaType & {
6666
options: Record<string, unknown>;
6767
};
@@ -70,36 +70,101 @@ describe('User Model', () => {
7070
});
7171

7272
describe('Database Connection State 2 Handling', () => {
73-
it('buffers operations when connection is in state 2 (connecting)', async () => {
74-
const { vi } = await import('vitest');
75-
const readyStateSpy = vi
73+
let readyStateSpy: ReturnType<typeof vi.spyOn> | undefined;
74+
75+
afterEach((): void => {
76+
// Restore bufferCommands to default (true) on both mongoose connection settings and the User schema
77+
mongoose.set('bufferCommands', true);
78+
User.schema.set('bufferCommands', true);
79+
80+
// Clean up collection queue to avoid leaking buffered operations to other tests
81+
const collectionWrapper = User.collection as unknown as { queue: unknown[] };
82+
if (collectionWrapper && Array.isArray(collectionWrapper.queue)) {
83+
collectionWrapper.queue = [];
84+
}
85+
86+
// Restore active state spies to ensure they never leak into surrounding test suites
87+
if (readyStateSpy) {
88+
readyStateSpy.mockRestore();
89+
readyStateSpy = undefined;
90+
}
91+
92+
// Clear all mocks to ensure absolute test isolation
93+
vi.clearAllMocks();
94+
});
95+
96+
it('buffers operations when connection is in state 2 (connecting) by default', async (): Promise<void> => {
97+
// In connection state 2 (connecting), Mongoose buffers operations by default rather than throwing errors immediately.
98+
// This behavior occurs because Mongoose assumes the database connection will be established shortly (transitioning to state 1).
99+
// Therefore, it queues all pending model commands inside the internal collection queue (User.collection.queue).
100+
//
101+
// In contrast, in connection state 0 (disconnected), Mongoose will either throw a ConnectionError immediately (if command buffering is disabled)
102+
// or time out because there is no ongoing connection attempt that would eventually flush the command buffer.
103+
// In state 2, the query remains in-flight (pending) waiting for connection recovery/open events.
104+
105+
let currentReadyState = 2;
106+
readyStateSpy = vi
76107
.spyOn(mongoose.connection, 'readyState', 'get')
77-
.mockReturnValue(2 as unknown as typeof mongoose.connection.readyState);
108+
.mockImplementation(() => currentReadyState as typeof mongoose.connection.readyState);
78109

79-
let operationAttempted = false;
110+
expect(mongoose.connection.readyState).toBe(2);
80111

81-
const simulateBufferedOperation = async () => {
82-
if (mongoose.connection.readyState === 2) {
83-
operationAttempted = true;
84-
return 'buffered';
85-
}
86-
return 'executed';
87-
};
112+
// Trigger a findOne operation which should transition smoothly into a buffered state
113+
const promise = User.findOne({ username: 'testuser' }).exec();
114+
115+
// Wait a microtask / tick for Mongoose to queue the collection operation
116+
await new Promise((resolve) => process.nextTick(resolve));
88117

89-
const result = await simulateBufferedOperation();
118+
// Assert that the command was successfully buffered in the collection wrapper queue
119+
const collectionWrapper = User.collection as unknown as { queue: unknown[] };
120+
expect(collectionWrapper.queue.length).toBe(1);
121+
122+
// Assert that the returned promise is pending by racing it with a fast timeout
123+
const timeoutPromise = new Promise((_, reject) =>
124+
setTimeout(() => reject(new Error('TIMEOUT')), 100)
125+
);
126+
await expect(Promise.race([promise, timeoutPromise])).rejects.toThrow('TIMEOUT');
127+
128+
// Simulate a successful connection transition to state 1 (connected)
129+
currentReadyState = 1;
130+
expect(mongoose.connection.readyState).toBe(1);
131+
132+
// Drain the queued operations on the connection. The query is now executed against the underlying driver.
133+
// Since there is no active physical database instance in this mock, it will fail during driver call dispatch,
134+
// confirming that the query promise was successfully released from the buffer and attempted execution.
135+
try {
136+
(mongoose.connection as unknown as { onOpen: () => void }).onOpen();
137+
await promise;
138+
} catch (error: unknown) {
139+
expect(error).toBeDefined();
140+
expect(error instanceof TypeError || error instanceof Error).toBe(true);
141+
}
142+
});
143+
144+
it('rejects operations immediately when bufferCommands is disabled in state 2', async (): Promise<void> => {
145+
// Disable command buffering on both mongoose and User schema
146+
mongoose.set('bufferCommands', false);
147+
User.schema.set('bufferCommands', false);
148+
149+
readyStateSpy = vi
150+
.spyOn(mongoose.connection, 'readyState', 'get')
151+
.mockReturnValue(2 as unknown as typeof mongoose.connection.readyState);
90152

91153
expect(mongoose.connection.readyState).toBe(2);
92-
expect(operationAttempted).toBe(true);
93-
expect(result).toBe('buffered');
94154

95-
readyStateSpy.mockRestore();
155+
// When buffering is disabled, the operation should fail immediately rather than waiting or queuing.
156+
await expect(User.findOne({ username: 'testuser' }).exec()).rejects.toThrow(
157+
/Cannot call.*if.*bufferCommands = false/
158+
);
159+
160+
// Assert that nothing was added to the collection queue
161+
const collectionWrapper = User.collection as unknown as { queue: unknown[] };
162+
expect(collectionWrapper.queue.length).toBe(0);
96163
});
97164
});
98165

99166
describe('Database Connection State 0 Handling', () => {
100-
it('fails queries gracefully with a ConnectionError when disconnected', async () => {
101-
const { vi } = await import('vitest');
102-
167+
it('fails queries gracefully with a ConnectionError when disconnected', async (): Promise<void> => {
103168
const readyStateSpy = vi
104169
.spyOn(mongoose.connection, 'readyState', 'get')
105170
.mockReturnValue(0 as unknown as typeof mongoose.connection.readyState);
@@ -124,9 +189,7 @@ describe('User Model', () => {
124189
});
125190

126191
describe('Database Connection State 3 (Disconnecting) Handling', () => {
127-
it('aborts/rolls back active transactions cleanly when connection is in state 3 (disconnecting)', async () => {
128-
const { vi } = await import('vitest');
129-
192+
it('aborts/rolls back active transactions cleanly when connection is in state 3 (disconnecting)', async (): Promise<void> => {
130193
const readyStateSpy = vi
131194
.spyOn(mongoose.connection, 'readyState', 'get')
132195
.mockReturnValue(3 as unknown as typeof mongoose.connection.readyState);
@@ -140,7 +203,9 @@ describe('User Model', () => {
140203

141204
const startSessionSpy = vi.spyOn(mongoose, 'startSession').mockResolvedValue(mockSession);
142205

143-
const runTransactionWithCheck = async (session: mongoose.ClientSession) => {
206+
const runTransactionWithCheck = async (
207+
session: mongoose.ClientSession
208+
): Promise<{ status: string }> => {
144209
session.startTransaction();
145210
try {
146211
if (mongoose.connection.readyState === 3) {
@@ -171,9 +236,7 @@ describe('User Model', () => {
171236
});
172237

173238
describe('Database Connection State 99 Handling', () => {
174-
it('triggers lazy initialization exactly once and uses the correct connection URI', async () => {
175-
const { vi } = await import('vitest');
176-
239+
it('triggers lazy initialization exactly once and uses the correct connection URI', async (): Promise<void> => {
177240
// 1. Mock readyState to 99 (uninitialized — no connection ever attempted)
178241
const readyStateSpy = vi
179242
.spyOn(mongoose.connection, 'readyState', 'get')
@@ -185,7 +248,7 @@ describe('User Model', () => {
185248
const MONGO_URI = 'mongodb://localhost:27017/commitpulse';
186249

187250
// 3. Simulate the lazy init fallback — connects exactly once with correct URI
188-
const lazyInit = async () => {
251+
const lazyInit = async (): Promise<void> => {
189252
if (mongoose.connection.readyState === 99) {
190253
await mongoose.connect(MONGO_URI);
191254
}

0 commit comments

Comments
 (0)