@vida-global/core 2.0.1 → 2.0.4
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/README.md +8 -7
- package/config/newrelic-config.js +2 -0
- package/lib/activeRecord/README.md +220 -111
- package/lib/http/README.md +84 -19
- package/lib/http/error.js +1 -1
- package/lib/jobQueue/README.md +139 -46
- package/lib/logger/README.md +48 -5
- package/lib/server/README.md +282 -129
- package/lib/server/controllerImporter.js +1 -1
- package/lib/server/controllerMixins/callbacks.js +132 -0
- package/lib/server/controllerMixins/documentation.js +36 -0
- package/lib/server/controllerMixins/renderer.js +315 -0
- package/lib/server/controllerMixins/requestDetails.js +82 -0
- package/lib/server/controllerMixins/routing.js +108 -0
- package/lib/server/controllerMixins/validations.js +114 -0
- package/lib/server/server.js +2 -0
- package/lib/server/serverController.js +68 -672
- package/package.json +8 -5
- package/test/http/error.test.js +1 -1
- package/test/http/helpers/error.js +5 -2
- package/test/server/serverController.test.js +822 -22
- package/test/apm/agent.test.js +0 -56
- package/test/apm/utils.test.js +0 -121
|
@@ -27,23 +27,37 @@ describe('VidaServerController', () => {
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
describe('.
|
|
30
|
+
describe('.autoLoadMixins', () => {
|
|
31
31
|
class AutoLoadTestController extends VidaServerController {
|
|
32
|
-
static get
|
|
32
|
+
static get mixinDirectory() {
|
|
33
33
|
return `${process.cwd()}/test/server/helpers/autoload/${this.name}.js`;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
it ('
|
|
37
|
+
it ('calls loadMixin with an empty mixin when there is no mixin file at the expected path', () => {
|
|
38
38
|
class TmpController extends AutoLoadTestController {}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const loadMixinSpy = jest.spyOn(TmpController, 'loadMixin');
|
|
40
|
+
|
|
41
|
+
TmpController.autoLoadMixins();
|
|
42
|
+
|
|
43
|
+
expect(loadMixinSpy).toHaveBeenCalledTimes(1);
|
|
44
|
+
expect(loadMixinSpy).toHaveBeenCalledWith({});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it ('does not add any own prototype or static properties when there is no mixin', () => {
|
|
48
|
+
class TmpController extends AutoLoadTestController {}
|
|
49
|
+
const protoKeysBefore = Object.getOwnPropertyNames(TmpController.prototype);
|
|
50
|
+
const staticKeysBefore = Object.getOwnPropertyNames(TmpController);
|
|
51
|
+
|
|
52
|
+
TmpController.autoLoadMixins();
|
|
53
|
+
|
|
54
|
+
expect(Object.getOwnPropertyNames(TmpController.prototype)).toEqual(protoKeysBefore);
|
|
55
|
+
expect(Object.getOwnPropertyNames(TmpController)).toEqual(staticKeysBefore);
|
|
42
56
|
});
|
|
43
57
|
|
|
44
|
-
it ('loads instance methods and accessors from the
|
|
58
|
+
it ('loads instance methods and accessors from the mixin', () => {
|
|
45
59
|
class TmpWithHelpersController extends AutoLoadTestController {}
|
|
46
|
-
TmpWithHelpersController.
|
|
60
|
+
TmpWithHelpersController.autoLoadMixins();
|
|
47
61
|
|
|
48
62
|
const { InstanceMethods, Accessors } = require('./helpers/autoload/TmpWithHelpersController');
|
|
49
63
|
|
|
@@ -350,7 +364,6 @@ describe('VidaServerController', () => {
|
|
|
350
364
|
const paramName2 = TestHelpers.Faker.Text.randomString();
|
|
351
365
|
const value1 = TestHelpers.Faker.Text.randomString();
|
|
352
366
|
const value2 = TestHelpers.Faker.Text.randomString();
|
|
353
|
-
const params = {[paramName1]: value1, [paramName2]: value2};
|
|
354
367
|
const validationType1 = TestHelpers.Faker.Text.randomString();
|
|
355
368
|
const validationType2 = TestHelpers.Faker.Text.randomString();
|
|
356
369
|
const options1 = [TestHelpers.Faker.Text.randomString()];
|
|
@@ -358,11 +371,13 @@ describe('VidaServerController', () => {
|
|
|
358
371
|
const error1 = TestHelpers.Faker.Text.randomString()
|
|
359
372
|
const error2 = TestHelpers.Faker.Text.randomString()
|
|
360
373
|
|
|
374
|
+
let params;
|
|
361
375
|
let controller;
|
|
362
376
|
let response;
|
|
363
377
|
let validator1;
|
|
364
378
|
let validator2;
|
|
365
379
|
beforeEach(() => {
|
|
380
|
+
params = {[paramName1]: value1, [paramName2]: value2};
|
|
366
381
|
response = {json: jest.fn(), statusCode: 200};
|
|
367
382
|
controller = new FooController({params}, response);
|
|
368
383
|
controller.render = jest.fn();
|
|
@@ -389,7 +404,14 @@ describe('VidaServerController', () => {
|
|
|
389
404
|
|
|
390
405
|
it ('throws a `ValidationError` when there\'s an error', async () => {
|
|
391
406
|
validator1.mockImplementation(() => error1);
|
|
392
|
-
await expect(controller.validateParameters(validations)).rejects.toThrow();
|
|
407
|
+
await expect(controller.validateParameters(validations)).rejects.toThrow(Errors.ValidationError);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it ('attaches the failing parameter and error message to the ValidationError fields', async () => {
|
|
411
|
+
validator1.mockImplementation(() => error1);
|
|
412
|
+
await expect(controller.validateParameters(validations)).rejects.toMatchObject({
|
|
413
|
+
fields: {[paramName1]: [error1], [paramName2]: [error1]}
|
|
414
|
+
});
|
|
393
415
|
});
|
|
394
416
|
|
|
395
417
|
it ('throws a `ValidationError` when there are multiple errors', async () => {
|
|
@@ -417,16 +439,86 @@ describe('VidaServerController', () => {
|
|
|
417
439
|
|
|
418
440
|
it ('does not error when optional parameters are not included', async () => {
|
|
419
441
|
const validations = {foo: {isString: true, optional: true}};
|
|
420
|
-
expect(controller.validateParameters(validations)).resolves.not.toThrow();
|
|
442
|
+
await expect(controller.validateParameters(validations)).resolves.not.toThrow();
|
|
421
443
|
});
|
|
422
444
|
|
|
423
|
-
it ('does
|
|
445
|
+
it ('does error when optional parameters are invalid', async () => {
|
|
424
446
|
const validations = {foo: {isString: {length: {gte: 2}}, optional: true}};
|
|
425
447
|
params.foo = '';
|
|
426
448
|
|
|
427
449
|
await expect(controller.validateParameters(validations)).rejects.toThrow(Errors.ValidationError);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
describe('.validateAction', () => {
|
|
455
|
+
it ('is a no-op when the controller has no parametersFor<Action> method', async () => {
|
|
456
|
+
class C extends VidaServerController {}
|
|
457
|
+
const controller = new C({}, {});
|
|
458
|
+
controller.validateParameters = jest.fn();
|
|
459
|
+
|
|
460
|
+
await controller.validateAction('getThing');
|
|
461
|
+
expect(controller.validateParameters).not.toHaveBeenCalled();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it ('calls validateParameters with the value returned by parametersFor<Action>', async () => {
|
|
465
|
+
const params = {foo: {isString: true}};
|
|
466
|
+
class C extends VidaServerController {
|
|
467
|
+
static parametersForGetThing() { return params; }
|
|
468
|
+
}
|
|
469
|
+
const controller = new C({}, {});
|
|
470
|
+
controller.validateParameters = jest.fn();
|
|
471
|
+
|
|
472
|
+
await controller.validateAction('getThing');
|
|
473
|
+
expect(controller.validateParameters).toHaveBeenCalledTimes(1);
|
|
474
|
+
expect(controller.validateParameters).toHaveBeenCalledWith(params);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it ('camelizes the action name when looking up the parametersFor method', async () => {
|
|
478
|
+
const params = {foo: {isString: true}};
|
|
479
|
+
class C extends VidaServerController {
|
|
480
|
+
static parametersForGetFooBar() { return params; }
|
|
481
|
+
}
|
|
482
|
+
const controller = new C({}, {});
|
|
483
|
+
controller.validateParameters = jest.fn();
|
|
484
|
+
|
|
485
|
+
await controller.validateAction('getFooBar');
|
|
486
|
+
expect(controller.validateParameters).toHaveBeenCalledWith(params);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it ('propagates ValidationError thrown by validateParameters', async () => {
|
|
490
|
+
class C extends VidaServerController {
|
|
491
|
+
static parametersForGetThing() { return {foo: {isString: true}}; }
|
|
492
|
+
}
|
|
493
|
+
const request = { params: {}, query: {}, body: {foo: 42} };
|
|
494
|
+
const controller = new C(request, {});
|
|
495
|
+
|
|
496
|
+
await expect(controller.validateAction('getThing')).rejects.toThrow(Errors.ValidationError);
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
describe('.parametersForAction', () => {
|
|
502
|
+
it ('returns null when no parametersFor method is defined', () => {
|
|
503
|
+
class C extends VidaServerController {}
|
|
504
|
+
expect(C.parametersForAction('getThing')).toBe(null);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it ('returns the value of the matching parametersFor<Action> method', () => {
|
|
508
|
+
const params = {foo: {isString: true}};
|
|
509
|
+
class C extends VidaServerController {
|
|
510
|
+
static parametersForGetThing() { return params; }
|
|
511
|
+
}
|
|
512
|
+
expect(C.parametersForAction('getThing')).toBe(params);
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
428
516
|
|
|
429
|
-
|
|
517
|
+
describe('.parametersMethodForAction', () => {
|
|
518
|
+
it ('returns the camelized parametersFor<Action> method name', () => {
|
|
519
|
+
class C extends VidaServerController {}
|
|
520
|
+
expect(C.parametersMethodForAction('getFooBar')).toBe('parametersForGetFooBar');
|
|
521
|
+
expect(C.parametersMethodForAction('postIndex')).toBe('parametersForPostIndex');
|
|
430
522
|
});
|
|
431
523
|
});
|
|
432
524
|
|
|
@@ -612,32 +704,32 @@ describe('VidaServerController', () => {
|
|
|
612
704
|
|
|
613
705
|
describe('.validateIsDateTime', () => {
|
|
614
706
|
const controller = new FooController({}, {});
|
|
615
|
-
it ('returns undefined when passed a string of format YYYY-MM-DD', () => {
|
|
707
|
+
it ('returns undefined when passed a date string of format YYYY-MM-DD', () => {
|
|
616
708
|
const result = controller.validateIsDateTime('2025-12-17');
|
|
617
709
|
expect(result).toBe(undefined);
|
|
618
710
|
});
|
|
619
711
|
|
|
620
|
-
it ('returns undefined when passed a string of format MM/DD/YY', () => {
|
|
712
|
+
it ('returns undefined when passed a date string of format MM/DD/YY', () => {
|
|
621
713
|
const result = controller.validateIsDateTime('12/17/25');
|
|
622
714
|
expect(result).toBe(undefined);
|
|
623
715
|
});
|
|
624
716
|
|
|
625
|
-
it ('returns undefined when passed a string of format Month name DD,
|
|
717
|
+
it ('returns undefined when passed a date string of format Month name DD, YYYY', () => {
|
|
626
718
|
const result = controller.validateIsDateTime('December 17, 2025');
|
|
627
719
|
expect(result).toBe(undefined);
|
|
628
720
|
});
|
|
629
721
|
|
|
630
|
-
it ('returns undefined when passed a string of format YYYY-MM-DD HH:MM:SS', () => {
|
|
722
|
+
it ('returns undefined when passed a datetime string of format YYYY-MM-DD HH:MM:SS', () => {
|
|
631
723
|
const result = controller.validateIsDateTime('2025-12-17 01:05:00');
|
|
632
724
|
expect(result).toBe(undefined);
|
|
633
725
|
});
|
|
634
726
|
|
|
635
|
-
it ('returns undefined when passed a string of format MM/DD/YY', () => {
|
|
727
|
+
it ('returns undefined when passed a datetime string of format MM/DD/YY HH:MM:SS', () => {
|
|
636
728
|
const result = controller.validateIsDateTime('12/17/25 01:05:00');
|
|
637
729
|
expect(result).toBe(undefined);
|
|
638
730
|
});
|
|
639
731
|
|
|
640
|
-
it ('returns undefined when passed a string of format Month name DD,
|
|
732
|
+
it ('returns undefined when passed a datetime string of format Month name DD, YYYY HH:MM:SS', () => {
|
|
641
733
|
const result = controller.validateIsDateTime('December 17, 2025 01:05:00');
|
|
642
734
|
expect(result).toBe(undefined);
|
|
643
735
|
});
|
|
@@ -701,7 +793,7 @@ describe('VidaServerController', () => {
|
|
|
701
793
|
const error = TestHelpers.Faker.Text.randomString();
|
|
702
794
|
const validator = (value) => value == correctValue ? undefined : error;
|
|
703
795
|
|
|
704
|
-
it ('returns
|
|
796
|
+
it ('returns undefined when passed a valid value', async () => {
|
|
705
797
|
const result = await controller.validateFunction(correctValue, validator);
|
|
706
798
|
expect(result).toBe(undefined);
|
|
707
799
|
});
|
|
@@ -771,6 +863,20 @@ describe('VidaServerController', () => {
|
|
|
771
863
|
expect(controller.statusCode).toEqual(500);
|
|
772
864
|
expect(controller.render).toHaveBeenCalledWith({errors: {message: null}});
|
|
773
865
|
});
|
|
866
|
+
|
|
867
|
+
it ('awaits render before calling response.end (regression: ERR_HTTP_HEADERS_SENT)', async () => {
|
|
868
|
+
const response = { json: jest.fn(), end: jest.fn(), statusCode: 200 };
|
|
869
|
+
const controller = new FooController({}, response);
|
|
870
|
+
controller.testAction = jest.fn(async () => ({foo: TestHelpers.Faker.Text.randomString()}));
|
|
871
|
+
|
|
872
|
+
await controller.performRequest('testAction');
|
|
873
|
+
|
|
874
|
+
expect(response.json).toHaveBeenCalledTimes(1);
|
|
875
|
+
expect(response.end).toHaveBeenCalledTimes(1);
|
|
876
|
+
const jsonOrder = response.json.mock.invocationCallOrder[0];
|
|
877
|
+
const endOrder = response.end.mock.invocationCallOrder[0];
|
|
878
|
+
expect(jsonOrder).toBeLessThan(endOrder);
|
|
879
|
+
});
|
|
774
880
|
});
|
|
775
881
|
|
|
776
882
|
|
|
@@ -922,6 +1028,118 @@ describe('VidaServerController', () => {
|
|
|
922
1028
|
});
|
|
923
1029
|
|
|
924
1030
|
|
|
1031
|
+
describe('#render', () => {
|
|
1032
|
+
let response;
|
|
1033
|
+
let controller;
|
|
1034
|
+
beforeEach(() => {
|
|
1035
|
+
response = {json: jest.fn(), send: jest.fn(), statusCode: 200};
|
|
1036
|
+
controller = new FooController({}, response);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
it ('calls response.send (not response.json) when the body is a string', async () => {
|
|
1040
|
+
const body = TestHelpers.Faker.Text.randomString();
|
|
1041
|
+
await controller.render(body);
|
|
1042
|
+
|
|
1043
|
+
expect(response.send).toHaveBeenCalledTimes(1);
|
|
1044
|
+
expect(response.send).toHaveBeenCalledWith(body);
|
|
1045
|
+
expect(response.json).not.toHaveBeenCalled();
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
it ('marks the controller as rendered after a string render', async () => {
|
|
1049
|
+
await controller.render('hello');
|
|
1050
|
+
expect(controller.rendered).toBe(true);
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it ('is a no-op when the controller has already been rendered', async () => {
|
|
1054
|
+
controller.markRendered();
|
|
1055
|
+
await controller.render('hello');
|
|
1056
|
+
expect(response.send).not.toHaveBeenCalled();
|
|
1057
|
+
expect(response.json).not.toHaveBeenCalled();
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it ('is a no-op when the controller is streaming', async () => {
|
|
1061
|
+
controller._isStreaming = true;
|
|
1062
|
+
await controller.render('hello');
|
|
1063
|
+
await controller.render({foo: 'bar'});
|
|
1064
|
+
|
|
1065
|
+
expect(response.send).not.toHaveBeenCalled();
|
|
1066
|
+
expect(response.json).not.toHaveBeenCalled();
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
it ('does not mark the controller as rendered when streaming', async () => {
|
|
1070
|
+
controller._isStreaming = true;
|
|
1071
|
+
await controller.render('hello');
|
|
1072
|
+
expect(controller.rendered).toBe(false);
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
describe('#processJSONBody', () => {
|
|
1078
|
+
let controller;
|
|
1079
|
+
beforeEach(() => {
|
|
1080
|
+
controller = new FooController({}, {});
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
it ('returns null when the body is undefined', async () => {
|
|
1084
|
+
expect(await controller.processJSONBody(undefined, {})).toBe(null);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
it ('returns null when the body is null', async () => {
|
|
1088
|
+
expect(await controller.processJSONBody(null, {})).toBe(null);
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
it ('returns primitives unchanged', async () => {
|
|
1092
|
+
expect(await controller.processJSONBody('hello', {})).toBe('hello');
|
|
1093
|
+
expect(await controller.processJSONBody(42, {})).toBe(42);
|
|
1094
|
+
expect(await controller.processJSONBody(true, {})).toBe(true);
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
it ('returns Date instances as their epoch ms via getTime', async () => {
|
|
1098
|
+
const date = new Date('2020-01-02T03:04:05Z');
|
|
1099
|
+
expect(await controller.processJSONBody(date, {})).toBe(date.getTime());
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
it ('recursively processes Date values inside objects', async () => {
|
|
1103
|
+
const date = new Date('2021-02-03T00:00:00Z');
|
|
1104
|
+
const result = await controller.processJSONBody({createdAt: date, name: 'foo'}, {});
|
|
1105
|
+
expect(result).toEqual({createdAt: date.getTime(), name: 'foo'});
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
it ('recursively processes arrays', async () => {
|
|
1109
|
+
const date = new Date('2022-03-04T00:00:00Z');
|
|
1110
|
+
const result = await controller.processJSONBody([1, date, {ts: date}], {});
|
|
1111
|
+
expect(result).toEqual([1, date.getTime(), {ts: date.getTime()}]);
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
it ('invokes toApiResponse on objects that define it and recurses on the result', async () => {
|
|
1115
|
+
const date = new Date('2023-04-05T00:00:00Z');
|
|
1116
|
+
const apiModel = {
|
|
1117
|
+
async toApiResponse() { return {id: 1, createdAt: date}; }
|
|
1118
|
+
};
|
|
1119
|
+
const result = await controller.processJSONBody(apiModel, {});
|
|
1120
|
+
expect(result).toEqual({id: 1, createdAt: date.getTime()});
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it ('passes the options argument through to toApiResponse', async () => {
|
|
1124
|
+
const options = {standardize: false, extra: 'flag'};
|
|
1125
|
+
const toApi = jest.fn().mockResolvedValue({});
|
|
1126
|
+
const apiModel = {toApiResponse: toApi};
|
|
1127
|
+
|
|
1128
|
+
await controller.processJSONBody(apiModel, options);
|
|
1129
|
+
expect(toApi).toHaveBeenCalledWith(options);
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
it ('recurses through arrays of objects with toApiResponse', async () => {
|
|
1133
|
+
const apiModels = [
|
|
1134
|
+
{toApiResponse: async () => ({id: 'a'})},
|
|
1135
|
+
{toApiResponse: async () => ({id: 'b'})},
|
|
1136
|
+
];
|
|
1137
|
+
const result = await controller.processJSONBody(apiModels, {});
|
|
1138
|
+
expect(result).toEqual([{id: 'a'}, {id: 'b'}]);
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
|
|
925
1143
|
describe('Request Properties', () => {
|
|
926
1144
|
let request;
|
|
927
1145
|
let response;
|
|
@@ -956,6 +1174,56 @@ describe('VidaServerController', () => {
|
|
|
956
1174
|
});
|
|
957
1175
|
|
|
958
1176
|
|
|
1177
|
+
describe('#userAgent', () => {
|
|
1178
|
+
it ('returns the user-agent request header', () => {
|
|
1179
|
+
const ua = TestHelpers.Faker.Text.randomString();
|
|
1180
|
+
request.headers['user-agent'] = ua;
|
|
1181
|
+
expect(controller.userAgent).toEqual(ua);
|
|
1182
|
+
});
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
describe('#requestBody', () => {
|
|
1187
|
+
it ('returns the underlying request body', () => {
|
|
1188
|
+
const body = {foo: TestHelpers.Faker.Text.randomString()};
|
|
1189
|
+
request.body = body;
|
|
1190
|
+
expect(controller.requestBody).toBe(body);
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
describe('#requestId', () => {
|
|
1196
|
+
it ('returns the request id', () => {
|
|
1197
|
+
request.id = TestHelpers.Faker.Text.randomString();
|
|
1198
|
+
expect(controller.requestId).toEqual(request.id);
|
|
1199
|
+
});
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
describe('#requestIp', () => {
|
|
1204
|
+
it ('returns the request ip', () => {
|
|
1205
|
+
request.ip = TestHelpers.Faker.Text.randomString();
|
|
1206
|
+
expect(controller.requestIp).toEqual(request.ip);
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
describe('#requestMethod', () => {
|
|
1212
|
+
it ('returns the request method', () => {
|
|
1213
|
+
request.method = 'POST';
|
|
1214
|
+
expect(controller.requestMethod).toEqual('POST');
|
|
1215
|
+
});
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
describe('#url', () => {
|
|
1220
|
+
it ('returns the request originalUrl', () => {
|
|
1221
|
+
request.originalUrl = `/${TestHelpers.Faker.Text.randomString()}`;
|
|
1222
|
+
expect(controller.url).toEqual(request.originalUrl);
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
|
|
959
1227
|
describe('#logger', () => {
|
|
960
1228
|
it ('returns a child of the passed logger, scoped to the request id', () => {
|
|
961
1229
|
const childLogger = {};
|
|
@@ -1000,23 +1268,368 @@ describe('VidaServerController', () => {
|
|
|
1000
1268
|
expect(response.statusCode).toEqual(code);
|
|
1001
1269
|
});
|
|
1002
1270
|
});
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
describe('#bearerToken', () => {
|
|
1274
|
+
beforeEach(() => {
|
|
1275
|
+
delete request.headers.authorization;
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
it ('returns null when there is no authorization header', () => {
|
|
1279
|
+
expect(controller.bearerToken).toBe(null);
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
it ('returns null when the authorization header is empty', () => {
|
|
1283
|
+
request.headers.authorization = '';
|
|
1284
|
+
expect(controller.bearerToken).toBe(null);
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
it ('returns null when the authorization scheme is not bearer', () => {
|
|
1288
|
+
request.headers.authorization = 'Basic dXNlcjpwYXNz';
|
|
1289
|
+
expect(controller.bearerToken).toBe(null);
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
it ('extracts the token after a lowercase bearer scheme', () => {
|
|
1293
|
+
request.headers.authorization = 'bearer abc123';
|
|
1294
|
+
expect(controller.bearerToken).toBe('abc123');
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
it ('extracts the token regardless of bearer scheme casing', () => {
|
|
1298
|
+
request.headers.authorization = 'Bearer abc123';
|
|
1299
|
+
expect(controller.bearerToken).toBe('abc123');
|
|
1300
|
+
|
|
1301
|
+
const c2 = new FooController({headers: {authorization: 'BEARER xyz'}}, response);
|
|
1302
|
+
expect(c2.bearerToken).toBe('xyz');
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
it ('tolerates leading whitespace and trims trailing whitespace from the token', () => {
|
|
1306
|
+
request.headers.authorization = ' Bearer tokenValue ';
|
|
1307
|
+
expect(controller.bearerToken).toBe('tokenValue');
|
|
1308
|
+
});
|
|
1309
|
+
});
|
|
1003
1310
|
});
|
|
1004
1311
|
|
|
1005
1312
|
|
|
1006
1313
|
describe('#setupCallbacks', () => {
|
|
1007
|
-
it ('is
|
|
1314
|
+
it ('is called once during construction and then cleared from the instance', () => {
|
|
1008
1315
|
class CallbacksTestController extends VidaServerController {}
|
|
1009
1316
|
CallbacksTestController.prototype.setupCallbacks = jest.fn();
|
|
1010
1317
|
const controller = new CallbacksTestController({}, {});
|
|
1011
1318
|
|
|
1012
1319
|
expect(CallbacksTestController.prototype.setupCallbacks).toHaveBeenCalledTimes(1);
|
|
1013
1320
|
expect(controller.setupCallbacks).toBe(undefined);
|
|
1321
|
+
});
|
|
1014
1322
|
|
|
1323
|
+
it ('is only called once per class across multiple instances', () => {
|
|
1324
|
+
class CallbacksOnceController extends VidaServerController {}
|
|
1325
|
+
CallbacksOnceController.prototype.setupCallbacks = jest.fn();
|
|
1326
|
+
|
|
1327
|
+
new CallbacksOnceController({}, {});
|
|
1328
|
+
new CallbacksOnceController({}, {});
|
|
1329
|
+
new CallbacksOnceController({}, {});
|
|
1330
|
+
|
|
1331
|
+
expect(CallbacksOnceController.prototype.setupCallbacks).toHaveBeenCalledTimes(1);
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
it ('does not duplicate registered callbacks across instances of the same class', async () => {
|
|
1335
|
+
const callback = jest.fn();
|
|
1336
|
+
|
|
1337
|
+
class CallbacksAccumController extends VidaServerController {
|
|
1338
|
+
setupCallbacks() { this.beforeCallback(callback); }
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
new CallbacksAccumController({}, {});
|
|
1342
|
+
new CallbacksAccumController({}, {});
|
|
1343
|
+
new CallbacksAccumController({}, {});
|
|
1344
|
+
const controller = new CallbacksAccumController({}, {});
|
|
1345
|
+
|
|
1346
|
+
await controller._runCallbacks('before', 'getIndex');
|
|
1347
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
it ('does not share callbacks across different controller classes', async () => {
|
|
1351
|
+
const callbackA = jest.fn();
|
|
1352
|
+
const callbackB = jest.fn();
|
|
1353
|
+
|
|
1354
|
+
class CallbacksClassA extends VidaServerController {
|
|
1355
|
+
setupCallbacks() { this.beforeCallback(callbackA); }
|
|
1356
|
+
}
|
|
1357
|
+
class CallbacksClassB extends VidaServerController {
|
|
1358
|
+
setupCallbacks() { this.beforeCallback(callbackB); }
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const a = new CallbacksClassA({}, {});
|
|
1362
|
+
const b = new CallbacksClassB({}, {});
|
|
1363
|
+
|
|
1364
|
+
await a._runCallbacks('before', 'someAction');
|
|
1365
|
+
expect(callbackA).toHaveBeenCalledTimes(1);
|
|
1366
|
+
expect(callbackB).toHaveBeenCalledTimes(0);
|
|
1367
|
+
|
|
1368
|
+
await b._runCallbacks('before', 'someAction');
|
|
1369
|
+
expect(callbackA).toHaveBeenCalledTimes(1);
|
|
1370
|
+
expect(callbackB).toHaveBeenCalledTimes(1);
|
|
1015
1371
|
});
|
|
1016
1372
|
});
|
|
1017
1373
|
|
|
1018
1374
|
|
|
1019
|
-
describe('
|
|
1375
|
+
describe('#_runCallbacks', () => {
|
|
1376
|
+
it ('runs before-callbacks in registration order then the action', async () => {
|
|
1377
|
+
const order = [];
|
|
1378
|
+
class C extends VidaServerController {
|
|
1379
|
+
setupCallbacks() {
|
|
1380
|
+
this.beforeCallback(() => { order.push('before1') });
|
|
1381
|
+
this.beforeCallback('namedBefore');
|
|
1382
|
+
}
|
|
1383
|
+
namedBefore() { order.push('before2') }
|
|
1384
|
+
testAction() { order.push('action'); return {} }
|
|
1385
|
+
}
|
|
1386
|
+
const response = { json: jest.fn(), end: jest.fn(), statusCode: 200 };
|
|
1387
|
+
const controller = new C({}, response);
|
|
1388
|
+
|
|
1389
|
+
await controller.performRequest('testAction');
|
|
1390
|
+
expect(order).toEqual(['before1', 'before2', 'action']);
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
it ('runs after-callbacks in registration order after the action', async () => {
|
|
1394
|
+
const order = [];
|
|
1395
|
+
class C extends VidaServerController {
|
|
1396
|
+
setupCallbacks() {
|
|
1397
|
+
this.afterCallback(() => { order.push('after1') });
|
|
1398
|
+
this.afterCallback('namedAfter');
|
|
1399
|
+
}
|
|
1400
|
+
namedAfter() { order.push('after2') }
|
|
1401
|
+
testAction() { order.push('action'); return {} }
|
|
1402
|
+
}
|
|
1403
|
+
const response = { json: jest.fn(), end: jest.fn(), statusCode: 200 };
|
|
1404
|
+
const controller = new C({}, response);
|
|
1405
|
+
|
|
1406
|
+
await controller.performRequest('testAction');
|
|
1407
|
+
expect(order).toEqual(['action', 'after1', 'after2']);
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
it ('skips the action when a before-callback returns false', async () => {
|
|
1411
|
+
const testAction = jest.fn();
|
|
1412
|
+
class C extends VidaServerController {
|
|
1413
|
+
setupCallbacks() { this.beforeCallback(() => false) }
|
|
1414
|
+
}
|
|
1415
|
+
C.prototype.testAction = testAction;
|
|
1416
|
+
const response = { json: jest.fn(), end: jest.fn(), statusCode: 200 };
|
|
1417
|
+
const controller = new C({}, response);
|
|
1418
|
+
|
|
1419
|
+
await controller.performRequest('testAction');
|
|
1420
|
+
expect(testAction).not.toHaveBeenCalled();
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
it ('skips remaining before-callbacks when one returns false', async () => {
|
|
1424
|
+
const second = jest.fn();
|
|
1425
|
+
class C extends VidaServerController {
|
|
1426
|
+
setupCallbacks() {
|
|
1427
|
+
this.beforeCallback(() => false);
|
|
1428
|
+
this.beforeCallback(second);
|
|
1429
|
+
}
|
|
1430
|
+
testAction() { return {} }
|
|
1431
|
+
}
|
|
1432
|
+
const response = { json: jest.fn(), end: jest.fn(), statusCode: 200 };
|
|
1433
|
+
const controller = new C({}, response);
|
|
1434
|
+
|
|
1435
|
+
await controller.performRequest('testAction');
|
|
1436
|
+
expect(second).not.toHaveBeenCalled();
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
it ('skips after-callbacks when a before-callback returns false', async () => {
|
|
1440
|
+
const after = jest.fn();
|
|
1441
|
+
class C extends VidaServerController {
|
|
1442
|
+
setupCallbacks() {
|
|
1443
|
+
this.beforeCallback(() => false);
|
|
1444
|
+
this.afterCallback(after);
|
|
1445
|
+
}
|
|
1446
|
+
testAction() { return {} }
|
|
1447
|
+
}
|
|
1448
|
+
const response = { json: jest.fn(), end: jest.fn(), statusCode: 200 };
|
|
1449
|
+
const controller = new C({}, response);
|
|
1450
|
+
|
|
1451
|
+
await controller.performRequest('testAction');
|
|
1452
|
+
expect(after).not.toHaveBeenCalled();
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
it ('respects only/except options when running callbacks', async () => {
|
|
1456
|
+
const onlyCb = jest.fn();
|
|
1457
|
+
const exceptCb = jest.fn();
|
|
1458
|
+
class C extends VidaServerController {
|
|
1459
|
+
setupCallbacks() {
|
|
1460
|
+
this.beforeCallback(onlyCb, {only: ['allowed']});
|
|
1461
|
+
this.beforeCallback(exceptCb, {except: ['blocked']});
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
const controller = new C({}, {});
|
|
1465
|
+
|
|
1466
|
+
await controller._runCallbacks('before', 'allowed');
|
|
1467
|
+
expect(onlyCb).toHaveBeenCalledTimes(1);
|
|
1468
|
+
expect(exceptCb).toHaveBeenCalledTimes(1);
|
|
1469
|
+
|
|
1470
|
+
await controller._runCallbacks('before', 'blocked');
|
|
1471
|
+
expect(onlyCb).toHaveBeenCalledTimes(1);
|
|
1472
|
+
expect(exceptCb).toHaveBeenCalledTimes(1);
|
|
1473
|
+
|
|
1474
|
+
await controller._runCallbacks('before', 'other');
|
|
1475
|
+
expect(onlyCb).toHaveBeenCalledTimes(1);
|
|
1476
|
+
expect(exceptCb).toHaveBeenCalledTimes(2);
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
it ('supports both function and method-name string callbacks', async () => {
|
|
1480
|
+
const fnCb = jest.fn();
|
|
1481
|
+
class C extends VidaServerController {
|
|
1482
|
+
setupCallbacks() {
|
|
1483
|
+
this.beforeCallback(fnCb);
|
|
1484
|
+
this.beforeCallback('namedCb');
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
C.prototype.namedCb = jest.fn();
|
|
1488
|
+
const controller = new C({}, {});
|
|
1489
|
+
|
|
1490
|
+
await controller._runCallbacks('before', 'someAction');
|
|
1491
|
+
expect(fnCb).toHaveBeenCalledTimes(1);
|
|
1492
|
+
expect(C.prototype.namedCb).toHaveBeenCalledTimes(1);
|
|
1493
|
+
});
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
describe('#skipBeforeCallback / #skipAfterCallback', () => {
|
|
1498
|
+
it ('skips a registered before-callback when the same function reference is in the skip list', async () => {
|
|
1499
|
+
const skipped = jest.fn();
|
|
1500
|
+
const kept = jest.fn();
|
|
1501
|
+
class C extends VidaServerController {
|
|
1502
|
+
setupCallbacks() {
|
|
1503
|
+
this.beforeCallback(skipped);
|
|
1504
|
+
this.beforeCallback(kept);
|
|
1505
|
+
this.skipBeforeCallback(skipped);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
const controller = new C({}, {});
|
|
1509
|
+
|
|
1510
|
+
await controller._runCallbacks('before', 'someAction');
|
|
1511
|
+
expect(skipped).not.toHaveBeenCalled();
|
|
1512
|
+
expect(kept).toHaveBeenCalledTimes(1);
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
it ('skips a registered before-callback by method-name string', async () => {
|
|
1516
|
+
class C extends VidaServerController {
|
|
1517
|
+
setupCallbacks() {
|
|
1518
|
+
this.beforeCallback('namedCb');
|
|
1519
|
+
this.skipBeforeCallback('namedCb');
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
C.prototype.namedCb = jest.fn();
|
|
1523
|
+
const controller = new C({}, {});
|
|
1524
|
+
|
|
1525
|
+
await controller._runCallbacks('before', 'someAction');
|
|
1526
|
+
expect(C.prototype.namedCb).not.toHaveBeenCalled();
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
it ('skips a registered after-callback when in the after skip list', async () => {
|
|
1530
|
+
const skipped = jest.fn();
|
|
1531
|
+
const kept = jest.fn();
|
|
1532
|
+
class C extends VidaServerController {
|
|
1533
|
+
setupCallbacks() {
|
|
1534
|
+
this.afterCallback(skipped);
|
|
1535
|
+
this.afterCallback(kept);
|
|
1536
|
+
this.skipAfterCallback(skipped);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
const controller = new C({}, {});
|
|
1540
|
+
|
|
1541
|
+
await controller._runCallbacks('after', 'someAction');
|
|
1542
|
+
expect(skipped).not.toHaveBeenCalled();
|
|
1543
|
+
expect(kept).toHaveBeenCalledTimes(1);
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
it ('only applies the skip on actions matching the skip\'s only option', async () => {
|
|
1547
|
+
const cb = jest.fn();
|
|
1548
|
+
class C extends VidaServerController {
|
|
1549
|
+
setupCallbacks() {
|
|
1550
|
+
this.beforeCallback(cb);
|
|
1551
|
+
this.skipBeforeCallback(cb, {only: 'skipMe'});
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
const controller = new C({}, {});
|
|
1555
|
+
|
|
1556
|
+
await controller._runCallbacks('before', 'skipMe');
|
|
1557
|
+
expect(cb).not.toHaveBeenCalled();
|
|
1558
|
+
|
|
1559
|
+
await controller._runCallbacks('before', 'otherAction');
|
|
1560
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
it ('applies the skip on all actions except those listed in the skip\'s except option', async () => {
|
|
1564
|
+
const cb = jest.fn();
|
|
1565
|
+
class C extends VidaServerController {
|
|
1566
|
+
setupCallbacks() {
|
|
1567
|
+
this.beforeCallback(cb);
|
|
1568
|
+
this.skipBeforeCallback(cb, {except: 'keepMe'});
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
const controller = new C({}, {});
|
|
1572
|
+
|
|
1573
|
+
await controller._runCallbacks('before', 'keepMe');
|
|
1574
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
1575
|
+
|
|
1576
|
+
await controller._runCallbacks('before', 'somethingElse');
|
|
1577
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
it ('does not affect callbacks whose identity does not match the skip entry', async () => {
|
|
1581
|
+
const cb = jest.fn();
|
|
1582
|
+
const other = jest.fn();
|
|
1583
|
+
class C extends VidaServerController {
|
|
1584
|
+
setupCallbacks() {
|
|
1585
|
+
this.beforeCallback(cb);
|
|
1586
|
+
this.skipBeforeCallback(other);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
const controller = new C({}, {});
|
|
1590
|
+
|
|
1591
|
+
await controller._runCallbacks('before', 'anyAction');
|
|
1592
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
1593
|
+
});
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
describe('#isAuthenticatedAction', () => {
|
|
1598
|
+
it ('returns false when no authenticateRequest callback is registered', () => {
|
|
1599
|
+
class NoAuthController extends VidaServerController {}
|
|
1600
|
+
const controller = new NoAuthController({}, {});
|
|
1601
|
+
expect(controller.isAuthenticatedAction('getIndex')).toBe(false);
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
it ('returns true for any action when authenticateRequest is registered without restrictions', () => {
|
|
1605
|
+
class AuthAllController extends VidaServerController {
|
|
1606
|
+
setupCallbacks() { this.beforeCallback('authenticateRequest'); }
|
|
1607
|
+
authenticateRequest() {}
|
|
1608
|
+
}
|
|
1609
|
+
const controller = new AuthAllController({}, {});
|
|
1610
|
+
expect(controller.isAuthenticatedAction('getIndex')).toBe(true);
|
|
1611
|
+
expect(controller.isAuthenticatedAction('getOther')).toBe(true);
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
it ('respects the `except` option on the authenticateRequest callback', () => {
|
|
1615
|
+
class AuthExceptController extends VidaServerController {
|
|
1616
|
+
setupCallbacks() { this.beforeCallback('authenticateRequest', {except: 'getOpen'}); }
|
|
1617
|
+
authenticateRequest() {}
|
|
1618
|
+
}
|
|
1619
|
+
const controller = new AuthExceptController({}, {});
|
|
1620
|
+
expect(controller.isAuthenticatedAction('getIndex')).toBe(true);
|
|
1621
|
+
expect(controller.isAuthenticatedAction('getOpen')).toBe(false);
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
it ('respects the `only` option on the authenticateRequest callback', () => {
|
|
1625
|
+
class AuthOnlyController extends VidaServerController {
|
|
1626
|
+
setupCallbacks() { this.beforeCallback('authenticateRequest', {only: ['getSecret']}); }
|
|
1627
|
+
authenticateRequest() {}
|
|
1628
|
+
}
|
|
1629
|
+
const controller = new AuthOnlyController({}, {});
|
|
1630
|
+
expect(controller.isAuthenticatedAction('getSecret')).toBe(true);
|
|
1631
|
+
expect(controller.isAuthenticatedAction('getIndex')).toBe(false);
|
|
1632
|
+
});
|
|
1020
1633
|
});
|
|
1021
1634
|
|
|
1022
1635
|
|
|
@@ -1210,4 +1823,191 @@ describe('VidaServerController', () => {
|
|
|
1210
1823
|
}, jest.fn(), abortController);
|
|
1211
1824
|
});
|
|
1212
1825
|
});
|
|
1826
|
+
|
|
1827
|
+
|
|
1828
|
+
describe('cookies / getCookie / setCookie', () => {
|
|
1829
|
+
let request;
|
|
1830
|
+
let response;
|
|
1831
|
+
let controller;
|
|
1832
|
+
|
|
1833
|
+
|
|
1834
|
+
beforeEach(() => {
|
|
1835
|
+
request = { cookies: { sessionId: 'abc', theme: 'dark' } };
|
|
1836
|
+
response = { cookie: jest.fn() };
|
|
1837
|
+
controller = new FooController(request, response);
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
|
|
1841
|
+
describe('#cookies', () => {
|
|
1842
|
+
it ('returns the cookies object from the request', () => {
|
|
1843
|
+
expect(controller.cookies).toEqual({ sessionId: 'abc', theme: 'dark' });
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
|
|
1847
|
+
it ('returns an empty object when request.cookies is undefined', () => {
|
|
1848
|
+
controller = new FooController({}, response);
|
|
1849
|
+
expect(controller.cookies).toEqual({});
|
|
1850
|
+
});
|
|
1851
|
+
});
|
|
1852
|
+
|
|
1853
|
+
|
|
1854
|
+
describe('#getCookie', () => {
|
|
1855
|
+
it ('returns the value for an existing cookie', () => {
|
|
1856
|
+
expect(controller.getCookie('sessionId')).toBe('abc');
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
|
|
1860
|
+
it ('returns null for a missing cookie', () => {
|
|
1861
|
+
expect(controller.getCookie('missing')).toBeNull();
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
|
|
1865
|
+
it ('returns null when name is empty', () => {
|
|
1866
|
+
expect(controller.getCookie('')).toBeNull();
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
|
|
1870
|
+
it ('returns null when name is null', () => {
|
|
1871
|
+
expect(controller.getCookie(null)).toBeNull();
|
|
1872
|
+
});
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
|
|
1876
|
+
describe('#setCookie', () => {
|
|
1877
|
+
it ('invokes response.cookie with the given name, value, and options', () => {
|
|
1878
|
+
const options = { path: '/foo', maxAge: 60000, httpOnly: true, secure: true, sameSite: 'None' };
|
|
1879
|
+
controller.setCookie('sessionId', 'xyz', options);
|
|
1880
|
+
expect(response.cookie).toHaveBeenCalledTimes(1);
|
|
1881
|
+
expect(response.cookie).toHaveBeenCalledWith('sessionId', 'xyz', options);
|
|
1882
|
+
});
|
|
1883
|
+
|
|
1884
|
+
|
|
1885
|
+
it ('defaults options to an empty object', () => {
|
|
1886
|
+
controller.setCookie('sessionId', 'xyz');
|
|
1887
|
+
expect(response.cookie).toHaveBeenCalledWith('sessionId', 'xyz', {});
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
it ('coerces null/undefined value to an empty string', () => {
|
|
1892
|
+
controller.setCookie('sessionId', null);
|
|
1893
|
+
expect(response.cookie).toHaveBeenCalledWith('sessionId', '', {});
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
|
|
1897
|
+
it ('is a no-op when name is empty', () => {
|
|
1898
|
+
controller.setCookie('', 'xyz');
|
|
1899
|
+
expect(response.cookie).not.toHaveBeenCalled();
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
|
|
1903
|
+
it ('is a no-op when name is null', () => {
|
|
1904
|
+
controller.setCookie(null, 'xyz');
|
|
1905
|
+
expect(response.cookie).not.toHaveBeenCalled();
|
|
1906
|
+
});
|
|
1907
|
+
});
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
|
|
1911
|
+
describe('#renderFile', () => {
|
|
1912
|
+
let request;
|
|
1913
|
+
let response;
|
|
1914
|
+
let controller;
|
|
1915
|
+
|
|
1916
|
+
|
|
1917
|
+
beforeEach(() => {
|
|
1918
|
+
request = {};
|
|
1919
|
+
response = {
|
|
1920
|
+
setHeader: jest.fn(),
|
|
1921
|
+
sendFile: jest.fn((path, options, cb) => cb()),
|
|
1922
|
+
};
|
|
1923
|
+
controller = new FooController(request, response);
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
|
|
1927
|
+
it ('sets Content-Type from the explicit option when provided', async () => {
|
|
1928
|
+
await controller.renderFile('/var/www/foo.bin', { contentType: 'application/x-custom' });
|
|
1929
|
+
expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'application/x-custom');
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
|
|
1933
|
+
it.each([
|
|
1934
|
+
['/a/b.html', 'text/html; charset=utf-8'],
|
|
1935
|
+
['/a/b.htm', 'text/html; charset=utf-8'],
|
|
1936
|
+
['/a/b.css', 'text/css; charset=utf-8'],
|
|
1937
|
+
['/a/b.js', 'application/javascript; charset=utf-8'],
|
|
1938
|
+
['/a/b.mjs', 'application/javascript; charset=utf-8'],
|
|
1939
|
+
['/a/b.json', 'application/json; charset=utf-8'],
|
|
1940
|
+
['/a/b.txt', 'text/plain; charset=utf-8'],
|
|
1941
|
+
['/a/b.png', 'image/png'],
|
|
1942
|
+
['/a/b.jpg', 'image/jpeg'],
|
|
1943
|
+
['/a/b.jpeg', 'image/jpeg'],
|
|
1944
|
+
['/a/b.gif', 'image/gif'],
|
|
1945
|
+
['/a/b.svg', 'image/svg+xml'],
|
|
1946
|
+
['/a/b.ico', 'image/x-icon'],
|
|
1947
|
+
['/a/b.webp', 'image/webp'],
|
|
1948
|
+
['/a/b.woff', 'font/woff'],
|
|
1949
|
+
['/a/b.woff2', 'font/woff2'],
|
|
1950
|
+
['/a/b.ttf', 'font/ttf'],
|
|
1951
|
+
['/a/b.otf', 'font/otf'],
|
|
1952
|
+
['/a/b.JPG', 'image/jpeg'],
|
|
1953
|
+
])('guesses Content-Type for %s', async (filePath, expected) => {
|
|
1954
|
+
await controller.renderFile(filePath);
|
|
1955
|
+
expect(response.setHeader).toHaveBeenCalledWith('Content-Type', expected);
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
|
|
1959
|
+
it ('falls back to application/octet-stream for unknown extensions', async () => {
|
|
1960
|
+
await controller.renderFile('/var/www/foo.xyz');
|
|
1961
|
+
expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'application/octet-stream');
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
it ('falls back to application/octet-stream when there is no extension', async () => {
|
|
1966
|
+
await controller.renderFile('/var/www/foo');
|
|
1967
|
+
expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'application/octet-stream');
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
|
|
1971
|
+
it ('sets Cache-Control when provided', async () => {
|
|
1972
|
+
await controller.renderFile('/var/www/foo.html', { cacheControl: 'no-store' });
|
|
1973
|
+
expect(response.setHeader).toHaveBeenCalledWith('Cache-Control', 'no-store');
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
|
|
1977
|
+
it ('does not set Cache-Control when omitted', async () => {
|
|
1978
|
+
await controller.renderFile('/var/www/foo.html');
|
|
1979
|
+
const cacheCalls = response.setHeader.mock.calls.filter(([name]) => name === 'Cache-Control');
|
|
1980
|
+
expect(cacheCalls).toHaveLength(0);
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
|
|
1984
|
+
it ('passes maxAge to res.sendFile', async () => {
|
|
1985
|
+
await controller.renderFile('/var/www/foo.html', { maxAge: 300000 });
|
|
1986
|
+
expect(response.sendFile).toHaveBeenCalledWith('/var/www/foo.html', { maxAge: 300000 }, expect.any(Function));
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
|
|
1990
|
+
it ('marks the controller as rendered', async () => {
|
|
1991
|
+
expect(controller.rendered).toBe(false);
|
|
1992
|
+
await controller.renderFile('/var/www/foo.html');
|
|
1993
|
+
expect(controller.rendered).toBe(true);
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
|
|
1997
|
+
it ('throws when filePath is empty', () => {
|
|
1998
|
+
expect(() => controller.renderFile('')).toThrow('renderFile requires a filePath');
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
|
|
2002
|
+
it ('throws when filePath is null', () => {
|
|
2003
|
+
expect(() => controller.renderFile(null)).toThrow('renderFile requires a filePath');
|
|
2004
|
+
});
|
|
2005
|
+
|
|
2006
|
+
|
|
2007
|
+
it ('rejects the returned promise when sendFile errors', async () => {
|
|
2008
|
+
const sendError = new Error('disk read failed');
|
|
2009
|
+
response.sendFile = jest.fn((path, options, cb) => cb(sendError));
|
|
2010
|
+
await expect(controller.renderFile('/var/www/foo.html')).rejects.toBe(sendError);
|
|
2011
|
+
});
|
|
2012
|
+
});
|
|
1213
2013
|
});
|