-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathdistributedLock.unit.tests.js
More file actions
160 lines (127 loc) · 4.68 KB
/
Copy pathdistributedLock.unit.tests.js
File metadata and controls
160 lines (127 loc) · 4.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/**
* Module dependencies.
*/
import { jest, describe, test, beforeEach, afterEach, expect } from '@jest/globals';
/**
* Unit tests for lib/services/distributedLock.js
*
* All Mongoose interactions are mocked — no real DB connection required.
* Tests verify the acquire / release contract and contention handling.
*/
describe('distributedLock — acquireLock:', () => {
let acquireLock;
let mockFindOneAndUpdate;
let mockDeleteOne;
beforeEach(async () => {
jest.resetModules();
mockFindOneAndUpdate = jest.fn();
mockDeleteOne = jest.fn();
const mockCronLock = {
findOneAndUpdate: mockFindOneAndUpdate,
deleteOne: mockDeleteOne,
};
jest.unstable_mockModule('mongoose', () => ({
default: {
Schema: class MockSchema {
constructor() {}
index() {}
},
models: {},
model: jest.fn(() => mockCronLock),
},
}));
({ acquireLock } = await import('../distributedLock.js'));
});
afterEach(() => {
jest.restoreAllMocks();
});
test('returns true when findOneAndUpdate resolves with matching holder', async () => {
mockFindOneAndUpdate.mockResolvedValue({ holder: 'pod-1' });
const ok = await acquireLock({ name: 'job-a', ttlMs: 60_000, holder: 'pod-1' });
expect(ok).toBe(true);
expect(mockFindOneAndUpdate).toHaveBeenCalledTimes(1);
const [filter, update, opts] = mockFindOneAndUpdate.mock.calls[0];
expect(filter._id).toBe('job-a');
expect(filter.lockedUntil.$lt).toBeInstanceOf(Date);
expect(update.$set.holder).toBe('pod-1');
expect(opts.upsert).toBe(true);
});
test('returns false when findOneAndUpdate returns doc held by different holder', async () => {
mockFindOneAndUpdate.mockResolvedValue({ holder: 'pod-1' });
const ok = await acquireLock({ name: 'job-b', ttlMs: 60_000, holder: 'pod-2' });
expect(ok).toBe(false);
});
test('returns false on E11000 duplicate-key (concurrent upsert race)', async () => {
const dupErr = new Error('E11000 duplicate key');
dupErr.code = 11000;
mockFindOneAndUpdate.mockRejectedValue(dupErr);
const ok = await acquireLock({ name: 'job-c', ttlMs: 60_000, holder: 'pod-1' });
expect(ok).toBe(false);
});
test('re-throws non-duplicate errors', async () => {
const dbErr = new Error('network timeout');
dbErr.code = 13;
mockFindOneAndUpdate.mockRejectedValue(dbErr);
await expect(acquireLock({ name: 'job-d', ttlMs: 60_000, holder: 'pod-1' })).rejects.toThrow('network timeout');
});
test.each([
[0, 'zero'],
[-1, 'negative'],
[Number.NaN, 'NaN'],
[Infinity, 'Infinity'],
[undefined, 'undefined'],
[null, 'null'],
])('throws when ttlMs is %s (%s)', async (ttlMs) => {
await expect(acquireLock({ name: 'job-guard', ttlMs, holder: 'pod-1' })).rejects.toThrow(
'acquireLock: ttlMs must be a positive number',
);
expect(mockFindOneAndUpdate).not.toHaveBeenCalled();
});
test('lockedUntil is set to now + ttlMs', async () => {
const before = Date.now();
mockFindOneAndUpdate.mockResolvedValue({ holder: 'pod-1' });
await acquireLock({ name: 'job-e', ttlMs: 10_000, holder: 'pod-1' });
const after = Date.now();
const { lockedUntil } = mockFindOneAndUpdate.mock.calls[0][1].$set;
expect(lockedUntil.getTime()).toBeGreaterThanOrEqual(before + 10_000);
expect(lockedUntil.getTime()).toBeLessThanOrEqual(after + 10_000);
});
});
describe('distributedLock — releaseLock:', () => {
let releaseLock;
let mockDeleteOne;
beforeEach(async () => {
jest.resetModules();
mockDeleteOne = jest.fn().mockResolvedValue({});
const mockCronLock = {
findOneAndUpdate: jest.fn(),
deleteOne: mockDeleteOne,
};
jest.unstable_mockModule('mongoose', () => ({
default: {
Schema: class MockSchema {
constructor() {}
index() {}
},
models: {},
model: jest.fn(() => mockCronLock),
},
}));
({ releaseLock } = await import('../distributedLock.js'));
});
afterEach(() => {
jest.restoreAllMocks();
});
test('calls deleteOne with name and holder', async () => {
await releaseLock({ name: 'job-a', holder: 'pod-1' });
expect(mockDeleteOne).toHaveBeenCalledWith({ _id: 'job-a', holder: 'pod-1' });
});
test('does not throw when deleteOne resolves', async () => {
await expect(releaseLock({ name: 'job-b', holder: 'pod-2' })).resolves.toBeUndefined();
});
test('propagates deleteOne errors to the caller', async () => {
const dbErr = new Error('network timeout');
mockDeleteOne.mockRejectedValue(dbErr);
await expect(releaseLock({ name: 'job-c', holder: 'pod-1' })).rejects.toThrow('network timeout');
});
});