@vida-global/core 1.4.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +13 -0
- package/README.md +1 -0
- package/config/newrelic-config.js +118 -0
- package/index.js +9 -5
- package/lib/activeRecord/baseRecord.js +49 -3
- package/lib/apm/agent.js +22 -0
- package/lib/apm/index.js +54 -0
- package/lib/apm/utils.js +43 -0
- package/lib/http/client.js +16 -2
- package/lib/jobQueue/README.md +63 -0
- package/lib/jobQueue/abstractJob.js +103 -0
- package/lib/jobQueue/abstractJobComponent.js +37 -0
- package/lib/jobQueue/index.js +10 -0
- package/lib/jobQueue/jobImporter.js +19 -0
- package/lib/jobQueue/queue.js +161 -0
- package/lib/jobQueue/worker.js +273 -0
- package/lib/logger/README.md +33 -2
- package/lib/logger/index.js +117 -20
- package/lib/logger/serverMiddleware.js +41 -0
- package/lib/redis/redisClient.js +32 -24
- package/lib/server/controllerImporter.js +9 -47
- package/lib/server/server.js +34 -71
- package/lib/server/serverController.js +215 -23
- package/lib/utils/abstractAutoImporter.js +64 -0
- package/package.json +8 -6
- package/test/activeRecord/baseRecord.test.js +129 -0
- package/test/activeRecord/db/connection.test.js +4 -1
- package/test/activeRecord/db/connectionConfiguration.test.js +9 -3
- package/test/activeRecord/db/schema.test.js +217 -0
- package/test/activeRecord/helpers/baseRecordMocks.js +6 -4
- package/test/activeRecord/helpers/connection.js +0 -3
- package/test/activeRecord/helpers/connectionConfiguration.js +0 -8
- package/test/activeRecord/utils.test.js +72 -0
- package/test/apm/agent.test.js +56 -0
- package/test/apm/utils.test.js +121 -0
- package/test/helpers/env.js +33 -0
- package/test/http/client.test.js +3 -3
- package/test/jobQueue/abstractJob.test.js +307 -0
- package/test/jobQueue/abstractJobComponent.test.js +110 -0
- package/test/jobQueue/helpers/abstractJob.js +26 -0
- package/test/jobQueue/helpers/apmMock.js +20 -0
- package/test/jobQueue/helpers/bullmqMock.js +67 -0
- package/test/jobQueue/helpers/env.js +29 -0
- package/test/jobQueue/helpers/fixtureJobs/notAJob.js +8 -0
- package/test/jobQueue/helpers/fixtureJobs/testJobA.js +14 -0
- package/test/jobQueue/helpers/fixtureJobs/testJobB.js +14 -0
- package/test/jobQueue/helpers/loggerMock.js +43 -0
- package/test/jobQueue/helpers/worker.js +38 -0
- package/test/jobQueue/queue.test.js +320 -0
- package/test/jobQueue/worker.test.js +526 -0
- package/test/logger/index.test.js +61 -0
- package/test/logger/serverMiddleware.test.js +72 -0
- package/test/server/apiDocsGenerator.test.js +38 -0
- package/test/server/server.test.js +186 -4
- package/test/server/serverController.test.js +407 -5
|
@@ -5,7 +5,11 @@ const Errors = require('../../lib/server/errors');
|
|
|
5
5
|
const { logger } = require('../../lib/logger');
|
|
6
6
|
const { randomPayload } = require('./helpers/serverController');
|
|
7
7
|
const TestHelpers = require('@vida-global/test-helpers');
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
NoActiveStreamError,
|
|
10
|
+
StreamInProgressError,
|
|
11
|
+
VidaServerController
|
|
12
|
+
} = require('../../lib/server/serverController');
|
|
9
13
|
|
|
10
14
|
describe('VidaServerController', () => {
|
|
11
15
|
describe('.actionNames', () => {
|
|
@@ -123,6 +127,178 @@ describe('VidaServerController', () => {
|
|
|
123
127
|
});
|
|
124
128
|
|
|
125
129
|
|
|
130
|
+
describe('#hasParam', () => {
|
|
131
|
+
let controller;
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
const params = {foo: 'bar', empty: '', falsy: false, nully: null};
|
|
134
|
+
controller = new FooController({params}, {});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it ('returns true when the named key exists in params', () => {
|
|
138
|
+
expect(controller.hasParam('foo')).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it ('returns true when the value is an empty string', () => {
|
|
142
|
+
expect(controller.hasParam('empty')).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it ('returns true when the value is null', () => {
|
|
146
|
+
expect(controller.hasParam('nully')).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it ('returns true when the value is false', () => {
|
|
150
|
+
expect(controller.hasParam('falsy')).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it ('returns false when the named key is not in params', () => {
|
|
154
|
+
expect(controller.hasParam('nope')).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
describe('#coerceBoolean', () => {
|
|
160
|
+
const controller = new FooController({}, {});
|
|
161
|
+
|
|
162
|
+
it ('returns true when passed boolean true', () => {
|
|
163
|
+
expect(controller.coerceBoolean(true)).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it ('returns false when passed boolean false', () => {
|
|
167
|
+
expect(controller.coerceBoolean(false)).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it.each([
|
|
171
|
+
['true'],
|
|
172
|
+
['TRUE'],
|
|
173
|
+
['1'],
|
|
174
|
+
['yes'],
|
|
175
|
+
['on'],
|
|
176
|
+
[' ON '],
|
|
177
|
+
])('coerces "%s" to true', (input) => {
|
|
178
|
+
expect(controller.coerceBoolean(input)).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it.each([
|
|
182
|
+
['false'],
|
|
183
|
+
['FALSE'],
|
|
184
|
+
['0'],
|
|
185
|
+
['no'],
|
|
186
|
+
['off'],
|
|
187
|
+
[' off '],
|
|
188
|
+
])('coerces "%s" to false', (input) => {
|
|
189
|
+
expect(controller.coerceBoolean(input)).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it ('returns the fallback for an unrecognized string', () => {
|
|
193
|
+
const fallback = TestHelpers.Faker.Text.randomString();
|
|
194
|
+
expect(controller.coerceBoolean('maybe', {fallback})).toBe(fallback);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it ('returns the fallback for an empty/whitespace string', () => {
|
|
198
|
+
const fallback = TestHelpers.Faker.Text.randomString();
|
|
199
|
+
expect(controller.coerceBoolean(' ', {fallback})).toBe(fallback);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it.each([
|
|
203
|
+
[null],
|
|
204
|
+
[undefined],
|
|
205
|
+
[123],
|
|
206
|
+
[{}],
|
|
207
|
+
[[]],
|
|
208
|
+
])('returns the fallback for non-string, non-boolean input (%p)', (input) => {
|
|
209
|
+
const fallback = TestHelpers.Faker.Text.randomString();
|
|
210
|
+
expect(controller.coerceBoolean(input, {fallback})).toBe(fallback);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it ('defaults the fallback to undefined when no options are given', () => {
|
|
214
|
+
expect(controller.coerceBoolean('maybe')).toBe(undefined);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
describe('#coerceInteger', () => {
|
|
220
|
+
const controller = new FooController({}, {});
|
|
221
|
+
|
|
222
|
+
it ('returns the fallback for null', () => {
|
|
223
|
+
const fallback = TestHelpers.Faker.Text.randomString();
|
|
224
|
+
expect(controller.coerceInteger(null, {fallback})).toBe(fallback);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it ('returns the fallback for undefined', () => {
|
|
228
|
+
const fallback = TestHelpers.Faker.Text.randomString();
|
|
229
|
+
expect(controller.coerceInteger(undefined, {fallback})).toBe(fallback);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it.each([
|
|
233
|
+
[''],
|
|
234
|
+
[' '],
|
|
235
|
+
])('returns the fallback for empty/whitespace string (%p)', (input) => {
|
|
236
|
+
const fallback = TestHelpers.Faker.Text.randomString();
|
|
237
|
+
expect(controller.coerceInteger(input, {fallback})).toBe(fallback);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it ('returns the fallback for a non-numeric string', () => {
|
|
241
|
+
const fallback = TestHelpers.Faker.Text.randomString();
|
|
242
|
+
expect(controller.coerceInteger('abc', {fallback})).toBe(fallback);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it ('parses an integer string to an integer', () => {
|
|
246
|
+
expect(controller.coerceInteger('42')).toBe(42);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it ('floors a decimal string value', () => {
|
|
250
|
+
expect(controller.coerceInteger('3.7')).toBe(3);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it ('accepts an integer value directly', () => {
|
|
254
|
+
expect(controller.coerceInteger(42)).toBe(42);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it ('floors a decimal numeric value', () => {
|
|
258
|
+
expect(controller.coerceInteger(3.7)).toBe(3);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it ('clamps to min when the value is below min', () => {
|
|
262
|
+
expect(controller.coerceInteger(1, {min: 5})).toBe(5);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it ('clamps to max when the value is above max', () => {
|
|
266
|
+
expect(controller.coerceInteger(100, {max: 50})).toBe(50);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it ('returns the fallback when nonNegative is set and the value is negative', () => {
|
|
270
|
+
const fallback = TestHelpers.Faker.Text.randomString();
|
|
271
|
+
expect(controller.coerceInteger(-3, {nonNegative: true, fallback})).toBe(fallback);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
describe('#coerceObject', () => {
|
|
277
|
+
const controller = new FooController({}, {});
|
|
278
|
+
|
|
279
|
+
it ('returns a plain object unchanged', () => {
|
|
280
|
+
const input = {foo: TestHelpers.Faker.Text.randomString()};
|
|
281
|
+
expect(controller.coerceObject(input)).toBe(input);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it.each([
|
|
285
|
+
[null],
|
|
286
|
+
[undefined],
|
|
287
|
+
[[1, 2]],
|
|
288
|
+
['string'],
|
|
289
|
+
[42],
|
|
290
|
+
[true],
|
|
291
|
+
])('returns the fallback for non-plain-object input (%p)', (input) => {
|
|
292
|
+
const fallback = {foo: TestHelpers.Faker.Text.randomString()};
|
|
293
|
+
expect(controller.coerceObject(input, {fallback})).toBe(fallback);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it ('defaults the fallback to an empty object', () => {
|
|
297
|
+
expect(controller.coerceObject(null)).toEqual({});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
|
|
126
302
|
describe('.validateParameters', () => {
|
|
127
303
|
const paramName1 = TestHelpers.Faker.Text.randomString();
|
|
128
304
|
const paramName2 = TestHelpers.Faker.Text.randomString();
|
|
@@ -444,6 +620,35 @@ describe('VidaServerController', () => {
|
|
|
444
620
|
});
|
|
445
621
|
|
|
446
622
|
|
|
623
|
+
describe('.validateIsBoolean', () => {
|
|
624
|
+
const controller = new FooController({}, {});
|
|
625
|
+
it ('returns undefined when passed a matching boolean', () => {
|
|
626
|
+
const result1 = controller.validateIsBoolean(true, true);
|
|
627
|
+
const result2 = controller.validateIsBoolean(false, false);
|
|
628
|
+
expect(result1).toBe(undefined);
|
|
629
|
+
expect(result2).toBe(undefined);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it ('returns an error when passed an non matching boolean', () => {
|
|
633
|
+
const result1 = controller.validateIsBoolean(true, false);
|
|
634
|
+
const result2 = controller.validateIsBoolean(false, true);
|
|
635
|
+
expect(result1).toBe('must be false');
|
|
636
|
+
expect(result2).toBe('must be true');
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it ('returns an error when passed an non boolean', () => {
|
|
640
|
+
const result1 = controller.validateIsBoolean('', false);
|
|
641
|
+
const result2 = controller.validateIsBoolean(0, false);
|
|
642
|
+
const result3 = controller.validateIsBoolean('foo', true);
|
|
643
|
+
const result4 = controller.validateIsBoolean(1, true);
|
|
644
|
+
expect(result1).toBe('must be false');
|
|
645
|
+
expect(result2).toBe('must be false');
|
|
646
|
+
expect(result3).toBe('must be true');
|
|
647
|
+
expect(result4).toBe('must be true');
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
|
|
447
652
|
describe('.validateFunction', () => {
|
|
448
653
|
const controller = new FooController({}, {});
|
|
449
654
|
const correctValue = Math.random();
|
|
@@ -494,8 +699,6 @@ describe('VidaServerController', () => {
|
|
|
494
699
|
const body = {[key]: value};
|
|
495
700
|
const message = TestHelpers.Faker.Text.randomString();
|
|
496
701
|
|
|
497
|
-
if (!Errors[errorClsName]) console.log(errorClsName);
|
|
498
|
-
|
|
499
702
|
const error = new Errors[errorClsName](message, body);
|
|
500
703
|
if (handlerName == 'renderErrors') error.errors = randomPayload();
|
|
501
704
|
|
|
@@ -708,8 +911,15 @@ describe('VidaServerController', () => {
|
|
|
708
911
|
|
|
709
912
|
|
|
710
913
|
describe('#logger', () => {
|
|
711
|
-
it ('
|
|
712
|
-
|
|
914
|
+
it ('returns a child of the passed logger, scoped to the request id', () => {
|
|
915
|
+
const childLogger = {};
|
|
916
|
+
const passedLogger = { child: jest.fn(() => childLogger) };
|
|
917
|
+
request.id = TestHelpers.Faker.Text.randomString();
|
|
918
|
+
|
|
919
|
+
const c = new FooController(request, response, passedLogger);
|
|
920
|
+
|
|
921
|
+
expect(passedLogger.child).toHaveBeenCalledWith(request.id);
|
|
922
|
+
expect(c.logger).toBe(childLogger);
|
|
713
923
|
});
|
|
714
924
|
});
|
|
715
925
|
|
|
@@ -762,4 +972,196 @@ describe('VidaServerController', () => {
|
|
|
762
972
|
|
|
763
973
|
describe('Request Properties', () => {
|
|
764
974
|
});
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
describe('#statusText', () => {
|
|
978
|
+
const cases = [
|
|
979
|
+
[200, 'ok'],
|
|
980
|
+
[201, 'created'],
|
|
981
|
+
[202, 'accepted'],
|
|
982
|
+
[204, 'no content'],
|
|
983
|
+
[301, 'moved permanently'],
|
|
984
|
+
[302, 'found'],
|
|
985
|
+
[304, 'not modified'],
|
|
986
|
+
[307, 'temporary redirect'],
|
|
987
|
+
[308, 'permanent redirect'],
|
|
988
|
+
[400, 'bad request'],
|
|
989
|
+
[401, 'unauthorized'],
|
|
990
|
+
[402, 'payment required'],
|
|
991
|
+
[403, 'forbidden'],
|
|
992
|
+
[404, 'not found'],
|
|
993
|
+
[405, 'method not allowed'],
|
|
994
|
+
[408, 'request timeout'],
|
|
995
|
+
[409, 'conflict'],
|
|
996
|
+
[410, 'gone'],
|
|
997
|
+
[413, 'content too large'],
|
|
998
|
+
[415, 'unsupported media type'],
|
|
999
|
+
[422, 'unprocessable content'],
|
|
1000
|
+
[423, 'locked'],
|
|
1001
|
+
[429, 'too many requests'],
|
|
1002
|
+
[500, 'server error'],
|
|
1003
|
+
[501, 'not implemented'],
|
|
1004
|
+
[502, 'bad gateway'],
|
|
1005
|
+
[503, 'service unavailable'],
|
|
1006
|
+
[504, 'gateway timeout'],
|
|
1007
|
+
];
|
|
1008
|
+
|
|
1009
|
+
it.each(cases)('returns "%s" text for status %s', (statusCode, expected) => {
|
|
1010
|
+
const controller = new FooController({}, { statusCode });
|
|
1011
|
+
expect(controller.statusText).toEqual(expected);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it ('returns undefined for an unmapped status code', () => {
|
|
1015
|
+
const controller = new FooController({}, { statusCode: 418 });
|
|
1016
|
+
expect(controller.statusText).toBeUndefined();
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
describe('#streamSseResponse', () => {
|
|
1022
|
+
let controller;
|
|
1023
|
+
let response;
|
|
1024
|
+
let request;
|
|
1025
|
+
let setHeaderSpy;
|
|
1026
|
+
|
|
1027
|
+
beforeEach(() => {
|
|
1028
|
+
request = { on: jest.fn(), off: jest.fn() };
|
|
1029
|
+
response = {
|
|
1030
|
+
setHeader: jest.fn(),
|
|
1031
|
+
flushHeaders: jest.fn(),
|
|
1032
|
+
write: jest.fn(),
|
|
1033
|
+
end: jest.fn(),
|
|
1034
|
+
writableEnded: false
|
|
1035
|
+
};
|
|
1036
|
+
controller = new FooController(request, response);
|
|
1037
|
+
setHeaderSpy = response.setHeader;
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
it ('sets SSE headers, flushes them, and marks rendered before invoking handler', async () => {
|
|
1041
|
+
const handler = jest.fn(async function () {
|
|
1042
|
+
expect(this).toBe(controller);
|
|
1043
|
+
expect(controller.rendered).toBe(true);
|
|
1044
|
+
expect(setHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/event-stream; charset=utf-8');
|
|
1045
|
+
expect(setHeaderSpy).toHaveBeenCalledWith('Cache-Control', 'no-cache');
|
|
1046
|
+
expect(setHeaderSpy).toHaveBeenCalledWith('Connection', 'keep-alive');
|
|
1047
|
+
expect(setHeaderSpy).toHaveBeenCalledWith('X-Accel-Buffering', 'no');
|
|
1048
|
+
expect(response.flushHeaders).toHaveBeenCalledTimes(1);
|
|
1049
|
+
expect(controller.statusCode).toBe(200);
|
|
1050
|
+
});
|
|
1051
|
+
await controller.streamSseResponse(handler, jest.fn());
|
|
1052
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it ('writes [DONE] and ends the response in the finally block', async () => {
|
|
1056
|
+
await controller.streamSseResponse(jest.fn(), jest.fn());
|
|
1057
|
+
expect(response.write).toHaveBeenLastCalledWith('data: [DONE]\n\n');
|
|
1058
|
+
expect(response.end).toHaveBeenCalledTimes(1);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
it ('throws StreamInProgressError when called while already streaming', async () => {
|
|
1062
|
+
await controller.streamSseResponse(async () => {
|
|
1063
|
+
await expect(controller.streamSseResponse(jest.fn(), jest.fn()))
|
|
1064
|
+
.rejects.toBeInstanceOf(StreamInProgressError);
|
|
1065
|
+
}, jest.fn());
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it ('invokes errorHandler and rethrows when handler throws', async () => {
|
|
1069
|
+
const err = new Error('boom');
|
|
1070
|
+
const handler = jest.fn(async () => { throw err; });
|
|
1071
|
+
const errorHandler = jest.fn();
|
|
1072
|
+
await expect(controller.streamSseResponse(handler, errorHandler)).rejects.toThrow('boom');
|
|
1073
|
+
expect(errorHandler).toHaveBeenCalledWith(err);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
it ('still ends the response when handler throws', async () => {
|
|
1077
|
+
const errorHandler = jest.fn();
|
|
1078
|
+
await expect(controller.streamSseResponse(
|
|
1079
|
+
async () => { throw new Error('boom'); },
|
|
1080
|
+
errorHandler
|
|
1081
|
+
)).rejects.toThrow();
|
|
1082
|
+
expect(response.end).toHaveBeenCalledTimes(1);
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
it ('wires close listener to abortController when one is provided', async () => {
|
|
1086
|
+
const abortController = { abort: jest.fn() };
|
|
1087
|
+
await controller.streamSseResponse(jest.fn(), jest.fn(), abortController);
|
|
1088
|
+
expect(request.on).toHaveBeenCalledWith('close', expect.any(Function));
|
|
1089
|
+
expect(request.off).toHaveBeenCalledWith('close', expect.any(Function));
|
|
1090
|
+
// The handler registered on 'close' should call abort.
|
|
1091
|
+
const closeHandler = request.on.mock.calls.find(c => c[0] === 'close')[1];
|
|
1092
|
+
closeHandler();
|
|
1093
|
+
expect(abortController.abort).toHaveBeenCalledTimes(1);
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
it ('does not register a close listener when no abortController is provided', async () => {
|
|
1097
|
+
await controller.streamSseResponse(jest.fn(), jest.fn());
|
|
1098
|
+
expect(request.on).not.toHaveBeenCalled();
|
|
1099
|
+
expect(request.off).not.toHaveBeenCalled();
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
it ('resets streaming state after completion so a new stream can start', async () => {
|
|
1103
|
+
await controller.streamSseResponse(jest.fn(), jest.fn());
|
|
1104
|
+
// Second call should not throw StreamInProgressError.
|
|
1105
|
+
await expect(controller.streamSseResponse(jest.fn(), jest.fn())).resolves.toBeUndefined();
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
describe('#writeStreamEvent', () => {
|
|
1111
|
+
let controller;
|
|
1112
|
+
let response;
|
|
1113
|
+
|
|
1114
|
+
beforeEach(() => {
|
|
1115
|
+
response = {
|
|
1116
|
+
setHeader: jest.fn(),
|
|
1117
|
+
flushHeaders: jest.fn(),
|
|
1118
|
+
write: jest.fn(),
|
|
1119
|
+
end: jest.fn(),
|
|
1120
|
+
writableEnded: false
|
|
1121
|
+
};
|
|
1122
|
+
controller = new FooController({ on: jest.fn(), off: jest.fn() }, response);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
it ('throws NoActiveStreamError when called outside a stream', () => {
|
|
1126
|
+
expect(() => controller.writeStreamEvent('msg', 'hello'))
|
|
1127
|
+
.toThrow(NoActiveStreamError);
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
it ('writes both event and data lines when an event name is provided', async () => {
|
|
1131
|
+
await controller.streamSseResponse(async () => {
|
|
1132
|
+
response.write.mockClear();
|
|
1133
|
+
controller.writeStreamEvent('progress', 'halfway');
|
|
1134
|
+
expect(response.write).toHaveBeenNthCalledWith(1, 'event: progress\n');
|
|
1135
|
+
expect(response.write).toHaveBeenNthCalledWith(2, 'data: halfway\n\n');
|
|
1136
|
+
}, jest.fn());
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it ('writes only the data line when no event name is provided', async () => {
|
|
1140
|
+
await controller.streamSseResponse(async () => {
|
|
1141
|
+
response.write.mockClear();
|
|
1142
|
+
controller.writeStreamEvent(null, 'plain');
|
|
1143
|
+
expect(response.write).toHaveBeenCalledTimes(1);
|
|
1144
|
+
expect(response.write).toHaveBeenCalledWith('data: plain\n\n');
|
|
1145
|
+
}, jest.fn());
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
it ('is a no-op when the response is already ended', async () => {
|
|
1149
|
+
await controller.streamSseResponse(async () => {
|
|
1150
|
+
response.writableEnded = true;
|
|
1151
|
+
response.write.mockClear();
|
|
1152
|
+
controller.writeStreamEvent(null, 'ignored');
|
|
1153
|
+
expect(response.write).not.toHaveBeenCalled();
|
|
1154
|
+
}, jest.fn());
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
it ('is a no-op when the abort signal is aborted', async () => {
|
|
1158
|
+
const abortController = { abort: jest.fn(), signal: { aborted: false } };
|
|
1159
|
+
await controller.streamSseResponse(async () => {
|
|
1160
|
+
abortController.signal.aborted = true;
|
|
1161
|
+
response.write.mockClear();
|
|
1162
|
+
controller.writeStreamEvent(null, 'ignored');
|
|
1163
|
+
expect(response.write).not.toHaveBeenCalled();
|
|
1164
|
+
}, jest.fn(), abortController);
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
765
1167
|
});
|