@toa.io/extensions.origins 0.7.2-dev.0 → 0.8.0-dev.1

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 (52) hide show
  1. package/package.json +9 -8
  2. package/readme.md +113 -0
  3. package/source/.deployment/index.js +5 -0
  4. package/source/.deployment/uris.js +35 -0
  5. package/source/.test/constants.js +3 -0
  6. package/source/.test/deployment.fixtures.js +20 -0
  7. package/source/.test/factory.fixtures.js +13 -0
  8. package/source/constants.js +3 -0
  9. package/source/deployment.js +28 -0
  10. package/source/deployment.test.js +158 -0
  11. package/source/env.js +50 -0
  12. package/source/factory.js +46 -0
  13. package/source/factory.test.js +140 -0
  14. package/{src → source}/index.js +2 -0
  15. package/source/manifest.js +25 -0
  16. package/source/manifest.test.js +57 -0
  17. package/source/protocols/amqp/.test/aspect.fixtures.js +15 -0
  18. package/source/protocols/amqp/.test/mock.comq.js +13 -0
  19. package/source/protocols/amqp/aspect.js +77 -0
  20. package/source/protocols/amqp/aspect.test.js +119 -0
  21. package/source/protocols/amqp/deployment.js +63 -0
  22. package/source/protocols/amqp/id.js +3 -0
  23. package/source/protocols/amqp/index.js +11 -0
  24. package/source/protocols/amqp/protocols.js +3 -0
  25. package/source/protocols/http/.aspect/permissions.js +65 -0
  26. package/{test → source/protocols/http/.test}/aspect.fixtures.js +6 -6
  27. package/source/protocols/http/aspect.js +129 -0
  28. package/source/protocols/http/aspect.test.js +239 -0
  29. package/source/protocols/http/id.js +3 -0
  30. package/source/protocols/http/index.js +9 -0
  31. package/source/protocols/http/protocols.js +3 -0
  32. package/source/protocols/index.js +6 -0
  33. package/source/schemas/annotations.cos.yaml +1 -0
  34. package/source/schemas/index.js +8 -0
  35. package/source/schemas/manifest.cos.yaml +1 -0
  36. package/types/amqp.d.ts +9 -0
  37. package/types/deployment.d.ts +7 -0
  38. package/types/http.d.ts +28 -0
  39. package/src/.manifest/index.js +0 -7
  40. package/src/.manifest/normalize.js +0 -17
  41. package/src/.manifest/schema.yaml +0 -13
  42. package/src/.manifest/validate.js +0 -13
  43. package/src/aspect.js +0 -95
  44. package/src/factory.js +0 -14
  45. package/src/manifest.js +0 -13
  46. package/test/aspect.test.js +0 -144
  47. package/test/factory.fixtures.js +0 -5
  48. package/test/factory.test.js +0 -22
  49. package/test/manifest.fixtures.js +0 -11
  50. package/test/manifest.test.js +0 -58
  51. package/types/aspect.ts +0 -19
  52. package/types/declaration.d.ts +0 -11
@@ -0,0 +1,239 @@
1
+ 'use strict'
2
+
3
+ const clone = require('clone-deep')
4
+ const { generate } = require('randomstring')
5
+ const { random } = require('@toa.io/generic')
6
+ const { Connector } = require('@toa.io/core')
7
+
8
+ /** @type {string[]} */
9
+ const protocols = require('../http/protocols')
10
+
11
+ const fixtures = require('./.test/aspect.fixtures')
12
+ const mock = fixtures.mock
13
+
14
+ jest.mock('node-fetch', () => mock.fetch)
15
+
16
+ const { create } = require('./aspect')
17
+
18
+ /** @type {toa.origins.http.Aspect} */ let aspect
19
+
20
+ beforeEach(() => {
21
+ jest.clearAllMocks()
22
+
23
+ aspect = create(fixtures.manifest)
24
+ })
25
+
26
+ it('should be instance of core.Connector', () => {
27
+ expect(aspect).toBeInstanceOf(Connector)
28
+ })
29
+
30
+ it('should have name \'origins\'', () => {
31
+ expect(aspect.name).toStrictEqual('http')
32
+ })
33
+
34
+ describe('invoke', () => {
35
+ const path = '/' + generate()
36
+ const headers = { [generate().toLowerCase()]: generate() }
37
+ const body = generate()
38
+
39
+ /** @type {import('node-fetch').RequestInit} */
40
+ const request = { method: 'PATCH', headers, body }
41
+ const name = 'foo'
42
+ const response = { [generate()]: generate() }
43
+
44
+ let call
45
+ let args
46
+ let result
47
+
48
+ beforeEach(async () => {
49
+ jest.clearAllMocks()
50
+ mock.fetch.reset()
51
+ mock.fetch.respond(200, response)
52
+
53
+ result = await aspect.invoke(name, path, clone(request))
54
+ call = mock.fetch.mock.calls[0]
55
+ args = call?.[1]
56
+ })
57
+
58
+ it('should throw on unknown origin', async () => {
59
+ await expect(() => aspect.invoke('bar', path, request)).rejects.toThrow(/is not defined/)
60
+ })
61
+
62
+ it('should resolve URL', async () => {
63
+ jest.clearAllMocks()
64
+ mock.fetch.respond(200, response)
65
+
66
+ const path = 'ok'
67
+
68
+ await aspect.invoke('deep', path, clone(request))
69
+
70
+ expect(mock.fetch.mock.calls[0][0]).toStrictEqual(fixtures.manifest.deep + path)
71
+ })
72
+
73
+ it.each(protocols)('should throw on absolute URL (%s)',
74
+ async (protocol) => {
75
+ jest.clearAllMocks()
76
+ mock.fetch.respond(200, response)
77
+
78
+ await expect(aspect.invoke('deep', protocol + '//api.domain.com', clone(request)))
79
+ .rejects.toThrow('Absolute URLs are forbidden')
80
+ })
81
+
82
+ it('should substitute wildcards', async () => {
83
+ jest.clearAllMocks()
84
+ mock.fetch.respond(200, response)
85
+
86
+ const substitutions = ['foo', 'bar', 443]
87
+
88
+ await aspect.invoke('amazon', path, clone(request), { substitutions })
89
+
90
+ const url = mock.fetch.mock.calls[0][0]
91
+
92
+ expect(url).toStrictEqual('https://foo.bar.amazon.com' + path)
93
+ })
94
+
95
+ it('should not lose query string', async () => {
96
+ jest.clearAllMocks()
97
+ mock.fetch.respond(200, response)
98
+
99
+ const path = generate() + '?foo=' + generate()
100
+
101
+ await aspect.invoke(name, path)
102
+
103
+ const url = mock.fetch.mock.calls[0][0]
104
+
105
+ expect(url).toStrictEqual(fixtures.manifest.foo + '/' + path)
106
+ })
107
+
108
+ it('should not throw if path is not defined', async () => {
109
+ jest.clearAllMocks()
110
+ mock.fetch.respond(200, response)
111
+
112
+ // noinspection JSCheckFunctionSignatures
113
+ expect(() => aspect.invoke(name)).not.toThrow()
114
+ })
115
+
116
+ describe('fetch', () => {
117
+ it('should fetch', async () => {
118
+ expect(mock.fetch).toHaveBeenCalledTimes(1)
119
+ })
120
+
121
+ it('should pass url', () => {
122
+ expect(call[0]).toStrictEqual(fixtures.manifest.foo + path)
123
+ })
124
+
125
+ it('should pass request', () => {
126
+ expect(args).toStrictEqual(request)
127
+ })
128
+
129
+ it('should return response', async () => {
130
+ const body = await result.json()
131
+
132
+ expect(body).toStrictEqual(response)
133
+ })
134
+ })
135
+
136
+ describe('retry', () => {
137
+ it('should retry', async () => {
138
+ jest.clearAllMocks()
139
+
140
+ const attempts = random(5) + 2
141
+
142
+ for (let i = 1; i < attempts; i++) mock.fetch.respond(500)
143
+
144
+ mock.fetch.respond(200, response)
145
+
146
+ /** @type {toa.origins.http.invocation.Options} */
147
+ const options = {
148
+ retry: { base: 0, retries: attempts }
149
+ }
150
+
151
+ await aspect.invoke(name, path, clone(request), options)
152
+
153
+ expect(mock.fetch).toHaveBeenCalledTimes(attempts)
154
+ })
155
+ })
156
+ })
157
+
158
+ describe.each(protocols)('absolute URL', (protocol) => {
159
+ const response = { [generate()]: generate() }
160
+
161
+ it('should request absolute URL', async () => {
162
+ mock.fetch.respond(200, response)
163
+
164
+ const properties = { null: true }
165
+ const url = protocol + '//' + generate()
166
+ const request = { method: 'POST' }
167
+
168
+ aspect = create(fixtures.manifest, properties)
169
+
170
+ await aspect.invoke(url, request)
171
+
172
+ expect(mock.fetch).toHaveBeenCalledWith(url, request)
173
+ })
174
+
175
+ it('should allow if TOA_DEV=1 and no properties', async () => {
176
+ const dev = process.env.TOA_DEV
177
+
178
+ process.env.TOA_DEV = '1'
179
+
180
+ mock.fetch.respond(200, response)
181
+
182
+ const url = protocol + '//' + generate()
183
+ const request = { method: 'POST' }
184
+
185
+ aspect = create(fixtures.manifest)
186
+
187
+ await aspect.invoke(url, request)
188
+
189
+ expect(mock.fetch).toHaveBeenCalledWith(url, request)
190
+
191
+ process.env.TOA_DEV = dev
192
+ })
193
+
194
+ it('should throw if URL not allowed', async () => {
195
+ mock.fetch.respond(200, response)
196
+
197
+ const properties = {}
198
+
199
+ aspect = create(fixtures.manifest, properties)
200
+
201
+ const url = protocol + '//' + generate()
202
+ const request = { method: 'POST' }
203
+
204
+ await expect(aspect.invoke(url, request)).rejects.toThrow('is not allowed')
205
+ })
206
+
207
+ it.each([
208
+ [String.raw`/^${protocol}\/\/api.\S+.com/`, `${protocol}//api.${generate()}.com/path/to`],
209
+ [String.raw`/${protocol}\/\/api.\S+.com/`, `${protocol}//api.${generate()}.com/path/to`]
210
+ ])('should allow requests %s', async (expression, url) => {
211
+ mock.fetch.respond(200, response)
212
+
213
+ const properties = { [expression]: true }
214
+
215
+ aspect = create(fixtures.manifest, properties)
216
+
217
+ await expect(aspect.invoke(url)).resolves.not.toThrow()
218
+ })
219
+
220
+ it.each([
221
+ [String.raw`/^${protocol}\/\/api.\S+.com/`, `${protocol}//api.${generate()}.com/path/to`],
222
+ [String.raw`/${protocol}\/\/api.\S+.com/`, `${protocol}//api.${generate()}.com/path/to`]
223
+ ])('should allow requests except %s', async (expression, url) => {
224
+ mock.fetch.respond(200, response)
225
+
226
+ const properties = { null: true, [expression]: false }
227
+
228
+ aspect = create(fixtures.manifest, properties)
229
+
230
+ await expect(aspect.invoke(url)).rejects.toThrow('is not allowed')
231
+ })
232
+
233
+ it.each([
234
+ ['starts', 'expression/'],
235
+ ['ends', '/expression']
236
+ ])('should throw if rule does not %s with /', async (_, expression) => {
237
+ expect(() => create(fixtures.manifest, { [expression]: true })).toThrow('is not a regular expression')
238
+ })
239
+ })
@@ -0,0 +1,3 @@
1
+ 'use strict'
2
+
3
+ exports.id = 'http'
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ const protocols = require('./protocols')
4
+ const { id } = require('./id')
5
+ const { create } = require('./aspect')
6
+
7
+ exports.protocols = protocols
8
+ exports.id = id
9
+ exports.create = create
@@ -0,0 +1,3 @@
1
+ 'use strict'
2
+
3
+ module.exports = ['http:', 'https:']
@@ -0,0 +1,6 @@
1
+ 'use strict'
2
+
3
+ const http = require('./http')
4
+ const amqp = require('./amqp')
5
+
6
+ module.exports = [http, amqp]
@@ -0,0 +1 @@
1
+ ~: ref:manifest
@@ -0,0 +1,8 @@
1
+ 'use strict'
2
+
3
+ const schemas = require('@toa.io/schemas')
4
+
5
+ const namespace = schemas.namespace(__dirname)
6
+
7
+ exports.manifest = namespace.schema('manifest')
8
+ exports.annotations = namespace.schema('annotations')
@@ -0,0 +1 @@
1
+ ~: uri
@@ -0,0 +1,9 @@
1
+ import * as _extensions from '@toa.io/core/types/extensions'
2
+
3
+ declare namespace toa.origins.amqp {
4
+
5
+ interface Aspect extends _extensions.Aspect {
6
+ invoke(origin: string, method: string, ...args: any[]): Promise<any>
7
+ }
8
+
9
+ }
@@ -0,0 +1,7 @@
1
+ declare namespace toa.origins {
2
+
3
+ type Manifest = Record<string, string>
4
+
5
+ type Annotations = Record<string, Manifest>
6
+
7
+ }
@@ -0,0 +1,28 @@
1
+ import * as fetch from 'node-fetch'
2
+ import * as _extensions from '@toa.io/core/types/extensions'
3
+ import * as _retry from '@toa.io/generic/types/retry'
4
+
5
+ declare namespace toa.origins.http {
6
+
7
+ namespace invocation {
8
+ type Options = {
9
+ substitutions?: string[]
10
+ retry?: _retry.Options
11
+ }
12
+ }
13
+
14
+ type Properties = Record<string | null, boolean>
15
+
16
+ interface Permissions {
17
+ test(url: string): boolean
18
+ }
19
+
20
+ interface Aspect extends _extensions.Aspect {
21
+ invoke(origin: string, path: string, request?: fetch.RequestInit, options?: invocation.Options): Promise<fetch.Response>
22
+
23
+ invoke(url: string, request?: fetch.RequestInit): Promise<fetch.Response>
24
+ }
25
+
26
+ }
27
+
28
+ export type Aspect = toa.origins.http.Aspect
@@ -1,7 +0,0 @@
1
- 'use strict'
2
-
3
- const { normalize } = require('./normalize')
4
- const { validate } = require('./validate')
5
-
6
- exports.normalize = normalize
7
- exports.validate = validate
@@ -1,17 +0,0 @@
1
- 'use strict'
2
-
3
- /**
4
- * @returns {toa.extensions.origins.Declaration}
5
- */
6
- const normalize = (declaration) => {
7
- declaration = origins(declaration)
8
-
9
- return declaration
10
- }
11
-
12
- const origins = (declaration) => {
13
- if (declaration.origins !== undefined) return declaration
14
- else return { origins: { ...declaration } }
15
- }
16
-
17
- exports.normalize = normalize
@@ -1,13 +0,0 @@
1
- $schema: https://json-schema.org/draft/2019-09/schema
2
- $id: https://schemas.toa.io/0.0.0/extensions/origins/declaration
3
-
4
- type: object
5
- properties:
6
- origins:
7
- type: object
8
- minProperties: 1
9
- patternProperties:
10
- '.*':
11
- type: string
12
- pattern: ^https?:\/\/(?=.{1,254}(?::|$))(?:(?![a-z0-9\-]{1,62}-(?:\.|:|$))[a-z0-9\-]{1,63}\b(?!\.$)\.?)+(:\d+)?$
13
- required: [origins]
@@ -1,13 +0,0 @@
1
- 'use strict'
2
-
3
- const path = require('path')
4
-
5
- const { Schema } = require('@toa.io/schema')
6
- const { load } = require('@toa.io/yaml')
7
-
8
- const schema = load.sync(path.resolve(__dirname, 'schema.yaml'))
9
- const validator = new Schema(schema)
10
-
11
- const validate = (declaration) => validator.validate(declaration)
12
-
13
- exports.validate = validate
package/src/aspect.js DELETED
@@ -1,95 +0,0 @@
1
- 'use strict'
2
-
3
- const fetch = require('node-fetch')
4
-
5
- const { Connector } = require('@toa.io/core')
6
- const { retry } = require('@toa.io/generic')
7
-
8
- /**
9
- * @implements {toa.extensions.origins.Aspect}
10
- */
11
- class Aspect extends Connector {
12
- /** @readonly */
13
- name = 'origins'
14
-
15
- /** @type {toa.extensions.origins.Origins} */
16
- #origins
17
-
18
- /**
19
- * @param {toa.extensions.origins.Declaration | Object} declaration
20
- */
21
- constructor (declaration) {
22
- super()
23
-
24
- this.#origins = declaration.origins
25
- }
26
-
27
- async invoke (name, path, request, options) {
28
- let origin = this.#origins[name]
29
-
30
- if (origin === undefined) throw new Error(`Origin '${name}' is not defined`)
31
-
32
- if (options?.substitutions !== undefined) origin = substitute(origin, options.substitutions)
33
-
34
- const url = new URL(origin)
35
-
36
- if (path !== undefined) append(url, path)
37
-
38
- return this.#request(url.href, request, options?.retry)
39
- }
40
-
41
- /**
42
- * @param {string} url
43
- * @param {import('node-fetch').RequestInit} request
44
- * @param {toa.generic.retry.Options} [options]
45
- * @return {Promise<import('node-fetch').Response>}
46
- */
47
- async #request (url, request, options) {
48
- const call = () => fetch(url, request)
49
-
50
- if (options === undefined) return call()
51
- else return this.#retry(call, options)
52
- }
53
-
54
- /**
55
- * @param {Function} call
56
- * @param {toa.generic.retry.Options} options
57
- * @return {any}
58
- */
59
- #retry (call, options) {
60
- return retry(async (retry) => {
61
- const response = await call()
62
-
63
- if (Math.floor(response.status / 100) !== 2) return retry()
64
-
65
- return response
66
- }, options)
67
- }
68
- }
69
-
70
- /**
71
- * @param {string} origin
72
- * @param {string[]} substitutions
73
- * @returns {string}
74
- */
75
- const substitute = (origin, substitutions) => {
76
- const replace = () => substitutions.shift()
77
-
78
- return origin.replace(PLACEHOLDER, replace)
79
- }
80
-
81
- /**
82
- * @param {URL} url
83
- * @param {string} path
84
- */
85
- const append = (url, path) => {
86
- const [pathname, search] = path.split('?')
87
-
88
- url.pathname = pathname
89
-
90
- if (search !== undefined) url.search = search
91
- }
92
-
93
- const PLACEHOLDER = /\*/g
94
-
95
- exports.Aspect = Aspect
package/src/factory.js DELETED
@@ -1,14 +0,0 @@
1
- 'use strict'
2
-
3
- const { Aspect } = require('./aspect')
4
-
5
- /**
6
- * @implements {toa.core.extensions.Factory}
7
- */
8
- class Factory {
9
- aspect (locator, declaration) {
10
- return new Aspect(declaration)
11
- }
12
- }
13
-
14
- exports.Factory = Factory
package/src/manifest.js DELETED
@@ -1,13 +0,0 @@
1
- 'use strict'
2
-
3
- const { normalize, validate } = require('./.manifest')
4
-
5
- const manifest = (declaration) => {
6
- declaration = normalize(declaration)
7
-
8
- validate(declaration)
9
-
10
- return declaration
11
- }
12
-
13
- exports.manifest = manifest
@@ -1,144 +0,0 @@
1
- 'use strict'
2
-
3
- const clone = require('clone-deep')
4
- const { generate } = require('randomstring')
5
- const { random } = require('@toa.io/generic')
6
-
7
- const { Connector } = require('@toa.io/core')
8
-
9
- const fixtures = require('./aspect.fixtures')
10
- const mock = fixtures.mock
11
-
12
- jest.mock('node-fetch', () => mock.fetch)
13
-
14
- const { Aspect } = require('../src/aspect')
15
-
16
- /** @type {toa.extensions.origins.Aspect} */ let aspect
17
-
18
- beforeEach(() => {
19
- jest.clearAllMocks()
20
-
21
- aspect = new Aspect(fixtures.declaration)
22
- })
23
-
24
- it('should be instance of core.Connector', () => {
25
- expect(aspect).toBeInstanceOf(Connector)
26
- })
27
-
28
- it('should have name \'origins\'', () => {
29
- expect(aspect.name).toStrictEqual('origins')
30
- })
31
-
32
- describe('invoke', () => {
33
- const path = '/' + generate()
34
- const headers = { [generate().toLowerCase()]: generate() }
35
- const body = generate()
36
-
37
- /** @type {import('node-fetch').RequestInit} */
38
- const request = { method: 'PATCH', headers, body }
39
- const name = 'foo'
40
- const response = { [generate()]: generate() }
41
-
42
- let call
43
- let args
44
- let result
45
-
46
- beforeEach(async () => {
47
- jest.clearAllMocks()
48
-
49
- mock.fetch.respond(200, response)
50
-
51
- result = await aspect.invoke(name, path, clone(request))
52
- call = mock.fetch.mock.calls[0]
53
- args = call?.[1]
54
- })
55
-
56
- it('should throw on unknown origin', async () => {
57
- await expect(() => aspect.invoke('bar', path, request)).rejects.toThrow(/is not defined/)
58
- })
59
-
60
- it('should not resolve absolute urls', async () => {
61
- jest.clearAllMocks()
62
- mock.fetch.respond(200, response)
63
-
64
- const path = 'https://toa.io'
65
-
66
- await aspect.invoke(name, path, clone(request))
67
-
68
- expect(mock.fetch.mock.calls[0][0]).toStrictEqual(fixtures.declaration.origins.foo + '/' + path)
69
- })
70
-
71
- it('should substitute wildcards', async () => {
72
- jest.clearAllMocks()
73
- mock.fetch.respond(200, response)
74
-
75
- const substitutions = ['foo', 'bar', 443]
76
-
77
- await aspect.invoke('amazon', path, clone(request), { substitutions })
78
-
79
- const url = mock.fetch.mock.calls[0][0]
80
-
81
- expect(url).toStrictEqual('https://foo.bar.amazon.com' + path)
82
- })
83
-
84
- it('should not lose query string', async () => {
85
- jest.clearAllMocks()
86
- mock.fetch.respond(200, response)
87
-
88
- const path = generate() + '?foo=' + generate()
89
-
90
- await aspect.invoke(name, path)
91
-
92
- const url = mock.fetch.mock.calls[0][0]
93
-
94
- expect(url).toStrictEqual(fixtures.declaration.origins.foo + '/' + path)
95
- })
96
-
97
- it('should not throw if path is not defined', async () => {
98
- jest.clearAllMocks()
99
- mock.fetch.respond(200, response)
100
-
101
- expect(() => aspect.invoke(name)).not.toThrow()
102
- })
103
-
104
- describe('fetch', () => {
105
- it('should fetch', async () => {
106
- expect(mock.fetch).toHaveBeenCalledTimes(1)
107
- })
108
-
109
- it('should pass url', () => {
110
- expect(call[0]).toStrictEqual(fixtures.declaration.origins.foo + path)
111
- })
112
-
113
- it('should pass request', () => {
114
- expect(args).toStrictEqual(request)
115
- })
116
-
117
- it('should return response', async () => {
118
- const body = await result.json()
119
-
120
- expect(body).toStrictEqual(response)
121
- })
122
- })
123
-
124
- describe('retry', () => {
125
- it('should retry', async () => {
126
- jest.clearAllMocks()
127
-
128
- const attempts = random(5) + 1
129
-
130
- for (let i = 1; i < attempts; i++) mock.fetch.respond(500)
131
-
132
- mock.fetch.respond(200, response)
133
-
134
- /** @type {toa.extensions.origins.invocation.Options} */
135
- const options = {
136
- retry: { base: 0, retries: attempts }
137
- }
138
-
139
- await aspect.invoke(name, path, clone(request), options)
140
-
141
- expect(mock.fetch).toHaveBeenCalledTimes(attempts)
142
- })
143
- })
144
- })
@@ -1,5 +0,0 @@
1
- 'use strict'
2
-
3
- const { declaration } = require('./manifest.fixtures')
4
-
5
- exports.declaration = declaration
@@ -1,22 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
-
5
- const { Aspect } = require('../src/aspect')
6
- const { Locator } = require('@toa.io/core')
7
-
8
- const fixtures = require('./factory.fixtures')
9
- const { Factory } = require('../src')
10
-
11
- /** @type {toa.core.extensions.Factory} */
12
- let factory
13
-
14
- beforeEach(() => {
15
- factory = new Factory()
16
- })
17
-
18
- it('should create context extension', () => {
19
- const extension = factory.aspect(new Locator(generate(), generate()), fixtures.declaration)
20
-
21
- expect(extension).toBeInstanceOf(Aspect)
22
- })