@vida-global/core 2.0.11 → 2.1.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.
Files changed (45) hide show
  1. package/lib/activeRecord/README.md +1 -1
  2. package/lib/activeRecord/baseRecord.js +70 -4
  3. package/lib/server/README.md +8 -219
  4. package/lib/server/controllerImporter.js +0 -1
  5. package/lib/server/controllerMixins/callbacks.js +16 -61
  6. package/lib/server/controllerMixins/classScopedRegistry.js +48 -0
  7. package/lib/server/controllerMixins/documentation.js +41 -9
  8. package/lib/server/controllerMixins/renderer.js +140 -99
  9. package/lib/server/controllerMixins/validations.js +57 -0
  10. package/lib/server/doc/callbacks.md +23 -0
  11. package/lib/server/doc/documentation.md +135 -0
  12. package/lib/server/doc/renderer.md +115 -0
  13. package/lib/server/doc/requestDetails.md +52 -0
  14. package/lib/server/doc/validations.md +40 -0
  15. package/lib/server/index.js +1 -1
  16. package/lib/server/openApi/apiDocGenerator.js +426 -0
  17. package/lib/server/openApi/apiDocsGenerator.js +147 -0
  18. package/lib/server/openApi/schemaImporter.js +41 -0
  19. package/lib/server/openApi/schemaRegistry.js +43 -0
  20. package/lib/server/openApi/schemas.js +97 -0
  21. package/lib/server/openApi/tagRegistry.js +27 -0
  22. package/lib/server/server.js +14 -0
  23. package/lib/server/serverController.js +4 -9
  24. package/lib/server/statusTexts.js +38 -0
  25. package/lib/utils/yamlLoader.js +17 -0
  26. package/package.json +4 -2
  27. package/test/activeRecord/baseRecord.test.js +101 -0
  28. package/test/server/apiDocGenerator.test.js +743 -0
  29. package/test/server/controllerMixins/callbacks.test.js +326 -0
  30. package/test/server/controllerMixins/classScopedRegistry.test.js +95 -0
  31. package/test/server/controllerMixins/documentation.test.js +168 -0
  32. package/test/server/controllerMixins/renderer.test.js +734 -0
  33. package/test/server/controllerMixins/requestDetails.test.js +225 -0
  34. package/test/server/controllerMixins/routing.test.js +82 -0
  35. package/test/server/controllerMixins/validations.test.js +509 -0
  36. package/test/server/openApi/apiDocsGenerator.test.js +306 -0
  37. package/test/server/openApi/helpers/apiDocsPackageFixture/package.json +5 -0
  38. package/test/server/openApi/schemaImporter.test.js +54 -0
  39. package/test/server/openApi/schemaRegistry.test.js +93 -0
  40. package/test/server/openApi/schemas.test.js +109 -0
  41. package/test/server/serverController.test.js +8 -867
  42. package/test/server/statusTexts.test.js +30 -0
  43. package/test/utils/yamlLoader.test.js +43 -0
  44. package/lib/server/apiDocsGenerator.js +0 -86
  45. package/test/server/apiDocsGenerator.test.js +0 -38
@@ -0,0 +1,326 @@
1
+ const { VidaServerController } = require('../../../lib/server/serverController');
2
+
3
+
4
+ describe('VidaServerController', () => {
5
+ describe('#setupCallbacks', () => {
6
+ it ('is called once during construction and then cleared from the instance', () => {
7
+ class CallbacksTestController extends VidaServerController {}
8
+ CallbacksTestController.prototype.setupCallbacks = jest.fn();
9
+ const controller = new CallbacksTestController({}, {});
10
+
11
+ expect(CallbacksTestController.prototype.setupCallbacks).toHaveBeenCalledTimes(1);
12
+ expect(controller.setupCallbacks).toBe(undefined);
13
+ });
14
+
15
+ it ('is only called once per class across multiple instances', () => {
16
+ class CallbacksOnceController extends VidaServerController {}
17
+ CallbacksOnceController.prototype.setupCallbacks = jest.fn();
18
+
19
+ new CallbacksOnceController({}, {});
20
+ new CallbacksOnceController({}, {});
21
+ new CallbacksOnceController({}, {});
22
+
23
+ expect(CallbacksOnceController.prototype.setupCallbacks).toHaveBeenCalledTimes(1);
24
+ });
25
+
26
+ it ('does not duplicate registered callbacks across instances of the same class', async () => {
27
+ const callback = jest.fn();
28
+
29
+ class CallbacksAccumController extends VidaServerController {
30
+ setupCallbacks() { this.beforeCallback(callback); }
31
+ }
32
+
33
+ new CallbacksAccumController({}, {});
34
+ new CallbacksAccumController({}, {});
35
+ new CallbacksAccumController({}, {});
36
+ const controller = new CallbacksAccumController({}, {});
37
+
38
+ await controller._runCallbacks('before', 'getIndex');
39
+ expect(callback).toHaveBeenCalledTimes(1);
40
+ });
41
+
42
+ it ('does not share callbacks across different controller classes', async () => {
43
+ const callbackA = jest.fn();
44
+ const callbackB = jest.fn();
45
+
46
+ class CallbacksClassA extends VidaServerController {
47
+ setupCallbacks() { this.beforeCallback(callbackA); }
48
+ }
49
+ class CallbacksClassB extends VidaServerController {
50
+ setupCallbacks() { this.beforeCallback(callbackB); }
51
+ }
52
+
53
+ const a = new CallbacksClassA({}, {});
54
+ const b = new CallbacksClassB({}, {});
55
+
56
+ await a._runCallbacks('before', 'someAction');
57
+ expect(callbackA).toHaveBeenCalledTimes(1);
58
+ expect(callbackB).toHaveBeenCalledTimes(0);
59
+
60
+ await b._runCallbacks('before', 'someAction');
61
+ expect(callbackA).toHaveBeenCalledTimes(1);
62
+ expect(callbackB).toHaveBeenCalledTimes(1);
63
+ });
64
+ });
65
+
66
+
67
+ describe('#_runCallbacks', () => {
68
+ it ('runs before-callbacks in registration order then the action', async () => {
69
+ const order = [];
70
+ class C extends VidaServerController {
71
+ setupCallbacks() {
72
+ this.beforeCallback(() => { order.push('before1') });
73
+ this.beforeCallback('namedBefore');
74
+ }
75
+ namedBefore() { order.push('before2') }
76
+ testAction() { order.push('action'); return {} }
77
+ }
78
+ const response = { json: jest.fn(), send: jest.fn(), setHeader: jest.fn(), end: jest.fn(), statusCode: 200 };
79
+ const controller = new C({}, response);
80
+
81
+ await controller.performRequest('testAction');
82
+ expect(order).toEqual(['before1', 'before2', 'action']);
83
+ });
84
+
85
+ it ('runs after-callbacks in registration order after the action', async () => {
86
+ const order = [];
87
+ class C extends VidaServerController {
88
+ setupCallbacks() {
89
+ this.afterCallback(() => { order.push('after1') });
90
+ this.afterCallback('namedAfter');
91
+ }
92
+ namedAfter() { order.push('after2') }
93
+ testAction() { order.push('action'); return {} }
94
+ }
95
+ const response = { json: jest.fn(), send: jest.fn(), setHeader: jest.fn(), end: jest.fn(), statusCode: 200 };
96
+ const controller = new C({}, response);
97
+
98
+ await controller.performRequest('testAction');
99
+ expect(order).toEqual(['action', 'after1', 'after2']);
100
+ });
101
+
102
+ it ('skips the action when a before-callback returns false', async () => {
103
+ const testAction = jest.fn();
104
+ class C extends VidaServerController {
105
+ setupCallbacks() { this.beforeCallback(() => false) }
106
+ }
107
+ C.prototype.testAction = testAction;
108
+ const response = { json: jest.fn(), send: jest.fn(), setHeader: jest.fn(), end: jest.fn(), statusCode: 200 };
109
+ const controller = new C({}, response);
110
+
111
+ await controller.performRequest('testAction');
112
+ expect(testAction).not.toHaveBeenCalled();
113
+ });
114
+
115
+ it ('skips remaining before-callbacks when one returns false', async () => {
116
+ const second = jest.fn();
117
+ class C extends VidaServerController {
118
+ setupCallbacks() {
119
+ this.beforeCallback(() => false);
120
+ this.beforeCallback(second);
121
+ }
122
+ testAction() { return {} }
123
+ }
124
+ const response = { json: jest.fn(), send: jest.fn(), setHeader: jest.fn(), end: jest.fn(), statusCode: 200 };
125
+ const controller = new C({}, response);
126
+
127
+ await controller.performRequest('testAction');
128
+ expect(second).not.toHaveBeenCalled();
129
+ });
130
+
131
+ it ('skips after-callbacks when a before-callback returns false', async () => {
132
+ const after = jest.fn();
133
+ class C extends VidaServerController {
134
+ setupCallbacks() {
135
+ this.beforeCallback(() => false);
136
+ this.afterCallback(after);
137
+ }
138
+ testAction() { return {} }
139
+ }
140
+ const response = { json: jest.fn(), send: jest.fn(), setHeader: jest.fn(), end: jest.fn(), statusCode: 200 };
141
+ const controller = new C({}, response);
142
+
143
+ await controller.performRequest('testAction');
144
+ expect(after).not.toHaveBeenCalled();
145
+ });
146
+
147
+ it ('respects only/except options when running callbacks', async () => {
148
+ const onlyCb = jest.fn();
149
+ const exceptCb = jest.fn();
150
+ class C extends VidaServerController {
151
+ setupCallbacks() {
152
+ this.beforeCallback(onlyCb, {only: ['allowed']});
153
+ this.beforeCallback(exceptCb, {except: ['blocked']});
154
+ }
155
+ }
156
+ const controller = new C({}, {});
157
+
158
+ await controller._runCallbacks('before', 'allowed');
159
+ expect(onlyCb).toHaveBeenCalledTimes(1);
160
+ expect(exceptCb).toHaveBeenCalledTimes(1);
161
+
162
+ await controller._runCallbacks('before', 'blocked');
163
+ expect(onlyCb).toHaveBeenCalledTimes(1);
164
+ expect(exceptCb).toHaveBeenCalledTimes(1);
165
+
166
+ await controller._runCallbacks('before', 'other');
167
+ expect(onlyCb).toHaveBeenCalledTimes(1);
168
+ expect(exceptCb).toHaveBeenCalledTimes(2);
169
+ });
170
+
171
+ it ('supports both function and method-name string callbacks', async () => {
172
+ const fnCb = jest.fn();
173
+ class C extends VidaServerController {
174
+ setupCallbacks() {
175
+ this.beforeCallback(fnCb);
176
+ this.beforeCallback('namedCb');
177
+ }
178
+ }
179
+ C.prototype.namedCb = jest.fn();
180
+ const controller = new C({}, {});
181
+
182
+ await controller._runCallbacks('before', 'someAction');
183
+ expect(fnCb).toHaveBeenCalledTimes(1);
184
+ expect(C.prototype.namedCb).toHaveBeenCalledTimes(1);
185
+ });
186
+ });
187
+
188
+
189
+ describe('#skipBeforeCallback / #skipAfterCallback', () => {
190
+ it ('skips a registered before-callback when the same function reference is in the skip list', async () => {
191
+ const skipped = jest.fn();
192
+ const kept = jest.fn();
193
+ class C extends VidaServerController {
194
+ setupCallbacks() {
195
+ this.beforeCallback(skipped);
196
+ this.beforeCallback(kept);
197
+ this.skipBeforeCallback(skipped);
198
+ }
199
+ }
200
+ const controller = new C({}, {});
201
+
202
+ await controller._runCallbacks('before', 'someAction');
203
+ expect(skipped).not.toHaveBeenCalled();
204
+ expect(kept).toHaveBeenCalledTimes(1);
205
+ });
206
+
207
+ it ('skips a registered before-callback by method-name string', async () => {
208
+ class C extends VidaServerController {
209
+ setupCallbacks() {
210
+ this.beforeCallback('namedCb');
211
+ this.skipBeforeCallback('namedCb');
212
+ }
213
+ }
214
+ C.prototype.namedCb = jest.fn();
215
+ const controller = new C({}, {});
216
+
217
+ await controller._runCallbacks('before', 'someAction');
218
+ expect(C.prototype.namedCb).not.toHaveBeenCalled();
219
+ });
220
+
221
+ it ('skips a registered after-callback when in the after skip list', async () => {
222
+ const skipped = jest.fn();
223
+ const kept = jest.fn();
224
+ class C extends VidaServerController {
225
+ setupCallbacks() {
226
+ this.afterCallback(skipped);
227
+ this.afterCallback(kept);
228
+ this.skipAfterCallback(skipped);
229
+ }
230
+ }
231
+ const controller = new C({}, {});
232
+
233
+ await controller._runCallbacks('after', 'someAction');
234
+ expect(skipped).not.toHaveBeenCalled();
235
+ expect(kept).toHaveBeenCalledTimes(1);
236
+ });
237
+
238
+ it ('only applies the skip on actions matching the skip\'s only option', async () => {
239
+ const cb = jest.fn();
240
+ class C extends VidaServerController {
241
+ setupCallbacks() {
242
+ this.beforeCallback(cb);
243
+ this.skipBeforeCallback(cb, {only: 'skipMe'});
244
+ }
245
+ }
246
+ const controller = new C({}, {});
247
+
248
+ await controller._runCallbacks('before', 'skipMe');
249
+ expect(cb).not.toHaveBeenCalled();
250
+
251
+ await controller._runCallbacks('before', 'otherAction');
252
+ expect(cb).toHaveBeenCalledTimes(1);
253
+ });
254
+
255
+ it ('applies the skip on all actions except those listed in the skip\'s except option', async () => {
256
+ const cb = jest.fn();
257
+ class C extends VidaServerController {
258
+ setupCallbacks() {
259
+ this.beforeCallback(cb);
260
+ this.skipBeforeCallback(cb, {except: 'keepMe'});
261
+ }
262
+ }
263
+ const controller = new C({}, {});
264
+
265
+ await controller._runCallbacks('before', 'keepMe');
266
+ expect(cb).toHaveBeenCalledTimes(1);
267
+
268
+ await controller._runCallbacks('before', 'somethingElse');
269
+ expect(cb).toHaveBeenCalledTimes(1);
270
+ });
271
+
272
+ it ('does not affect callbacks whose identity does not match the skip entry', async () => {
273
+ const cb = jest.fn();
274
+ const other = jest.fn();
275
+ class C extends VidaServerController {
276
+ setupCallbacks() {
277
+ this.beforeCallback(cb);
278
+ this.skipBeforeCallback(other);
279
+ }
280
+ }
281
+ const controller = new C({}, {});
282
+
283
+ await controller._runCallbacks('before', 'anyAction');
284
+ expect(cb).toHaveBeenCalledTimes(1);
285
+ });
286
+ });
287
+
288
+
289
+ describe('#isAuthenticatedAction', () => {
290
+ it ('returns false when no authenticateRequest callback is registered', () => {
291
+ class NoAuthController extends VidaServerController {}
292
+ const controller = new NoAuthController({}, {});
293
+ expect(controller.isAuthenticatedAction('getIndex')).toBe(false);
294
+ });
295
+
296
+ it ('returns true for any action when authenticateRequest is registered without restrictions', () => {
297
+ class AuthAllController extends VidaServerController {
298
+ setupCallbacks() { this.beforeCallback('authenticateRequest'); }
299
+ authenticateRequest() {}
300
+ }
301
+ const controller = new AuthAllController({}, {});
302
+ expect(controller.isAuthenticatedAction('getIndex')).toBe(true);
303
+ expect(controller.isAuthenticatedAction('getOther')).toBe(true);
304
+ });
305
+
306
+ it ('respects the `except` option on the authenticateRequest callback', () => {
307
+ class AuthExceptController extends VidaServerController {
308
+ setupCallbacks() { this.beforeCallback('authenticateRequest', {except: 'getOpen'}); }
309
+ authenticateRequest() {}
310
+ }
311
+ const controller = new AuthExceptController({}, {});
312
+ expect(controller.isAuthenticatedAction('getIndex')).toBe(true);
313
+ expect(controller.isAuthenticatedAction('getOpen')).toBe(false);
314
+ });
315
+
316
+ it ('respects the `only` option on the authenticateRequest callback', () => {
317
+ class AuthOnlyController extends VidaServerController {
318
+ setupCallbacks() { this.beforeCallback('authenticateRequest', {only: ['getSecret']}); }
319
+ authenticateRequest() {}
320
+ }
321
+ const controller = new AuthOnlyController({}, {});
322
+ expect(controller.isAuthenticatedAction('getSecret')).toBe(true);
323
+ expect(controller.isAuthenticatedAction('getIndex')).toBe(false);
324
+ });
325
+ });
326
+ });
@@ -0,0 +1,95 @@
1
+ const { ClassScopedRegistry, appliesToAction } = require('../../../lib/server/controllerMixins/classScopedRegistry');
2
+
3
+
4
+ describe('appliesToAction', () => {
5
+ it.each([
6
+ ['no options matches every action', {}, 'getIndex', true],
7
+ ['only string matches the named action', { only: 'getIndex' }, 'getIndex', true],
8
+ ['only string excludes other actions', { only: 'getIndex' }, 'getRecord', false],
9
+ ['only array matches a listed action', { only: ['getIndex', 'a'] }, 'a', true],
10
+ ['only array excludes unlisted actions', { only: ['getIndex', 'a'] }, 'getRecord', false],
11
+ ['except string excludes the named one', { except: 'getIndex' }, 'getIndex', false],
12
+ ['except string matches other actions', { except: 'getIndex' }, 'getRecord', true],
13
+ ['except array excludes a listed action', { except: ['a', 'b'] }, 'b', false],
14
+ ['except array matches unlisted actions', { except: ['a', 'b'] }, 'getRecord', true],
15
+ ])('%s', (_label, options, actionName, expected) => {
16
+ expect(appliesToAction(actionName, options)).toBe(expected);
17
+ });
18
+ });
19
+
20
+
21
+ describe('ClassScopedRegistry', () => {
22
+ describe('#list', () => {
23
+ it ('returns the same array for repeated calls with the same name and class', () => {
24
+ const registry = new ClassScopedRegistry();
25
+ const controller = { constructor: class A {} };
26
+
27
+ const first = registry.list('callbacks', controller);
28
+ const second = registry.list('callbacks', controller);
29
+
30
+ expect(second).toBe(first);
31
+ });
32
+
33
+ it ('isolates lists by controller class', () => {
34
+ const registry = new ClassScopedRegistry();
35
+ const a = { constructor: class A {} };
36
+ const b = { constructor: class B {} };
37
+
38
+ registry.list('callbacks', a).push('from-a');
39
+
40
+ expect(registry.list('callbacks', b)).toEqual([]);
41
+ });
42
+
43
+ it ('isolates lists by name within the same class', () => {
44
+ const registry = new ClassScopedRegistry();
45
+ const controller = { constructor: class A {} };
46
+
47
+ registry.list('before', controller).push('x');
48
+
49
+ expect(registry.list('after', controller)).toEqual([]);
50
+ });
51
+ });
52
+
53
+
54
+ describe('#applySetupOnce', () => {
55
+ it ('runs the hook once per class across multiple instances', () => {
56
+ const registry = new ClassScopedRegistry();
57
+ class Controller {}
58
+ Controller.prototype.setup = jest.fn();
59
+
60
+ const a = new Controller();
61
+ registry.applySetupOnce(a, 'setup');
62
+ const b = new Controller();
63
+ registry.applySetupOnce(b, 'setup');
64
+
65
+ expect(Controller.prototype.setup).toHaveBeenCalledTimes(1);
66
+ });
67
+
68
+ it ('clears the hook from the instance after applying', () => {
69
+ const registry = new ClassScopedRegistry();
70
+ class Controller {}
71
+ Controller.prototype.setup = jest.fn();
72
+ const controller = new Controller();
73
+
74
+ registry.applySetupOnce(controller, 'setup');
75
+
76
+ expect(controller.setup).toBe(undefined);
77
+ });
78
+
79
+ it ('runs the hook again for a different class', () => {
80
+ const registry = new ClassScopedRegistry();
81
+ const setupA = jest.fn();
82
+ const setupB = jest.fn();
83
+ class A {}
84
+ class B {}
85
+ A.prototype.setup = setupA;
86
+ B.prototype.setup = setupB;
87
+
88
+ registry.applySetupOnce(new A(), 'setup');
89
+ registry.applySetupOnce(new B(), 'setup');
90
+
91
+ expect(setupA).toHaveBeenCalledTimes(1);
92
+ expect(setupB).toHaveBeenCalledTimes(1);
93
+ });
94
+ });
95
+ });
@@ -0,0 +1,168 @@
1
+ const { VidaServerController } = require('../../../lib/server/serverController');
2
+
3
+
4
+ describe('VidaServerController', () => {
5
+ describe('#setupDocumentation', () => {
6
+ it ('is called once during construction and then cleared from the instance', () => {
7
+ class DocSetupController extends VidaServerController {}
8
+ DocSetupController.prototype.setupDocumentation = jest.fn();
9
+ const controller = new DocSetupController({}, {});
10
+
11
+ expect(DocSetupController.prototype.setupDocumentation).toHaveBeenCalledTimes(1);
12
+ expect(controller.setupDocumentation).toBe(undefined);
13
+ });
14
+
15
+ it ('is only called once per class across multiple instances', () => {
16
+ class DocOnceController extends VidaServerController {}
17
+ DocOnceController.prototype.setupDocumentation = jest.fn();
18
+
19
+ new DocOnceController({}, {});
20
+ new DocOnceController({}, {});
21
+ new DocOnceController({}, {});
22
+
23
+ expect(DocOnceController.prototype.setupDocumentation).toHaveBeenCalledTimes(1);
24
+ });
25
+
26
+ it ('does not duplicate registered tags across instances of the same class', () => {
27
+ class DocAccumController extends VidaServerController {
28
+ setupDocumentation() { this.documentationTag('Things'); }
29
+ }
30
+
31
+ new DocAccumController({}, {});
32
+ new DocAccumController({}, {});
33
+ const controller = new DocAccumController({}, {});
34
+
35
+ expect(controller.tagsForAction('getIndex')).toEqual(['Things']);
36
+ });
37
+
38
+ it ('does not share tags across different controller classes', () => {
39
+ class DocClassA extends VidaServerController {
40
+ setupDocumentation() { this.documentationTag('A'); }
41
+ }
42
+ class DocClassB extends VidaServerController {
43
+ setupDocumentation() { this.documentationTag('B'); }
44
+ }
45
+
46
+ const a = new DocClassA({}, {});
47
+ const b = new DocClassB({}, {});
48
+
49
+ expect(a.tagsForAction('someAction')).toEqual(['A']);
50
+ expect(b.tagsForAction('someAction')).toEqual(['B']);
51
+ });
52
+ });
53
+
54
+
55
+ describe('#tagsForAction', () => {
56
+ it ('returns every declared tag when no only/except option is set', () => {
57
+ class DocAllController extends VidaServerController {
58
+ setupDocumentation() {
59
+ this.documentationTag('Things');
60
+ this.documentationTag('Admin');
61
+ }
62
+ }
63
+ const controller = new DocAllController({}, {});
64
+
65
+ expect(controller.tagsForAction('getIndex')).toEqual(['Things', 'Admin']);
66
+ });
67
+
68
+ it ('restricts a tag to the action named in a string only option', () => {
69
+ class DocOnlyStringController extends VidaServerController {
70
+ setupDocumentation() { this.documentationTag('Admin', { only: 'deleteRecord' }); }
71
+ }
72
+ const controller = new DocOnlyStringController({}, {});
73
+
74
+ expect(controller.tagsForAction('deleteRecord')).toEqual(['Admin']);
75
+ expect(controller.tagsForAction('getIndex')).toEqual([]);
76
+ });
77
+
78
+ it ('restricts a tag to the actions listed in an array only option', () => {
79
+ class DocOnlyArrayController extends VidaServerController {
80
+ setupDocumentation() { this.documentationTag('Admin', { only: ['deleteRecord', 'putRecord'] }); }
81
+ }
82
+ const controller = new DocOnlyArrayController({}, {});
83
+
84
+ expect(controller.tagsForAction('deleteRecord')).toEqual(['Admin']);
85
+ expect(controller.tagsForAction('putRecord')).toEqual(['Admin']);
86
+ expect(controller.tagsForAction('getIndex')).toEqual([]);
87
+ });
88
+
89
+ it ('excludes the action named in a string except option', () => {
90
+ class DocExceptStringController extends VidaServerController {
91
+ setupDocumentation() { this.documentationTag('Users', { except: 'getHealth' }); }
92
+ }
93
+ const controller = new DocExceptStringController({}, {});
94
+
95
+ expect(controller.tagsForAction('getIndex')).toEqual(['Users']);
96
+ expect(controller.tagsForAction('getHealth')).toEqual([]);
97
+ });
98
+
99
+ it ('excludes the actions listed in an array except option', () => {
100
+ class DocExceptArrayController extends VidaServerController {
101
+ setupDocumentation() { this.documentationTag('Users', { except: ['getHealth', 'getStatus'] }); }
102
+ }
103
+ const controller = new DocExceptArrayController({}, {});
104
+
105
+ expect(controller.tagsForAction('getIndex')).toEqual(['Users']);
106
+ expect(controller.tagsForAction('getHealth')).toEqual([]);
107
+ expect(controller.tagsForAction('getStatus')).toEqual([]);
108
+ });
109
+
110
+ it ('returns an empty list for a controller that declares no tag', () => {
111
+ class DocNoneController extends VidaServerController {}
112
+ const controller = new DocNoneController({}, {});
113
+
114
+ expect(controller.tagsForAction('anyAction')).toEqual([]);
115
+ });
116
+ });
117
+
118
+
119
+ describe('._tagsDocumentation', () => {
120
+ it ('returns the tag names honoring only/except for the given action', () => {
121
+ class DocStaticController extends VidaServerController {
122
+ setupDocumentation() {
123
+ this.documentationTag('Users');
124
+ this.documentationTag('Admin', { only: 'deleteRecord' });
125
+ }
126
+ }
127
+
128
+ expect(DocStaticController._tagsDocumentation('getIndex')).toEqual(['Users']);
129
+ expect(DocStaticController._tagsDocumentation('deleteRecord')).toEqual(['Users', 'Admin']);
130
+ });
131
+
132
+ it ('returns an empty list for a controller with no tags', () => {
133
+ class DocStaticNoneController extends VidaServerController {}
134
+
135
+ expect(DocStaticNoneController._tagsDocumentation('anyAction')).toEqual([]);
136
+ });
137
+ });
138
+
139
+
140
+ describe('.documentationTagDefinitions', () => {
141
+ it ('returns a de-duplicated catalogue including the option-supplied description', () => {
142
+ class DocDefsController extends VidaServerController {
143
+ setupDocumentation() {
144
+ this.documentationTag('Users', { description: 'User endpoints', except: 'getHealth' });
145
+ this.documentationTag('Admin', { only: 'deleteRecord' });
146
+ }
147
+ }
148
+
149
+ expect(DocDefsController.documentationTagDefinitions()).toEqual([
150
+ { name: 'Users', description: 'User endpoints' },
151
+ { name: 'Admin', description: '' },
152
+ ]);
153
+ });
154
+
155
+ it ('keeps the first description when the same tag is declared twice', () => {
156
+ class DocDefsDupController extends VidaServerController {
157
+ setupDocumentation() {
158
+ this.documentationTag('Users', { description: 'First' });
159
+ this.documentationTag('Users', { description: 'Second' });
160
+ }
161
+ }
162
+
163
+ expect(DocDefsDupController.documentationTagDefinitions()).toEqual([
164
+ { name: 'Users', description: 'First' },
165
+ ]);
166
+ });
167
+ });
168
+ });