@checkdigit/eslint-plugin 6.6.0-PR.75-e80b → 6.6.0-PR.75-3e31
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/dist-cjs/index.cjs +177 -161
- package/dist-cjs/metafile.json +40 -3
- package/dist-mjs/ast/format.mjs +13 -0
- package/dist-mjs/ast/tree.mjs +18 -0
- package/dist-mjs/no-fixture.mjs +143 -149
- package/dist-types/ast/format.d.ts +3 -0
- package/dist-types/ast/tree.d.ts +3 -0
- package/package.json +1 -1
- package/src/ast/format.ts +19 -0
- package/src/ast/tree.ts +29 -0
- package/src/no-fixture.ts +231 -225
package/src/no-fixture.ts
CHANGED
|
@@ -10,55 +10,65 @@
|
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
12
|
AwaitExpression,
|
|
13
|
+
CallExpression,
|
|
13
14
|
Expression,
|
|
14
15
|
MemberExpression,
|
|
15
|
-
Node,
|
|
16
16
|
ReturnStatement,
|
|
17
17
|
SimpleCallExpression,
|
|
18
|
+
VariableDeclaration,
|
|
18
19
|
} from 'estree';
|
|
19
20
|
import type { Rule, Scope, SourceCode } from 'eslint';
|
|
21
|
+
import { getAncestor, getParent } from './ast/tree';
|
|
20
22
|
import { strict as assert } from 'node:assert';
|
|
21
23
|
import getDocumentationUrl from './get-documentation-url';
|
|
24
|
+
import { getIndentation } from './ast/format';
|
|
22
25
|
|
|
23
26
|
export const ruleId = 'no-fixture';
|
|
24
27
|
|
|
25
|
-
type NodeParent = Node | undefined | null;
|
|
26
|
-
|
|
27
|
-
interface NodeParentExtension {
|
|
28
|
-
parent: NodeParent;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
28
|
interface FixtureCallInformation {
|
|
32
|
-
|
|
29
|
+
rootNode: AwaitExpression | ReturnStatement | VariableDeclaration;
|
|
30
|
+
fixtureNode: AwaitExpression | SimpleCallExpression;
|
|
31
|
+
variableDeclaration?: VariableDeclaration;
|
|
33
32
|
requestBody?: Expression;
|
|
34
33
|
requestHeaders?: { name: Expression; value: Expression }[];
|
|
35
34
|
assertions?: Expression[][];
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function analyze(call: SimpleCallExpression, results: FixtureCallInformation) {
|
|
37
|
+
// recursively analyze the fixture/supertest call chain to collect information of request/response
|
|
38
|
+
function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation) {
|
|
43
39
|
const parent = getParent(call);
|
|
44
40
|
assert.ok(parent, 'parent should exist for fixture/supertest call node');
|
|
45
41
|
|
|
46
42
|
let nextCall;
|
|
47
|
-
if (parent.type === '
|
|
48
|
-
//
|
|
49
|
-
results.
|
|
43
|
+
if (parent.type === 'ReturnStatement') {
|
|
44
|
+
// direct return, no variable declaration / await
|
|
45
|
+
results.fixtureNode = call;
|
|
46
|
+
results.rootNode = parent;
|
|
47
|
+
} else if (parent.type === 'AwaitExpression') {
|
|
48
|
+
results.fixtureNode = call;
|
|
49
|
+
// [TODO:] should we consider variable declaration without await??
|
|
50
|
+
const variableDeclaration = getAncestor(parent, 'VariableDeclaration', 'FunctionDeclaration');
|
|
51
|
+
if (variableDeclaration?.type === 'VariableDeclaration') {
|
|
52
|
+
results.variableDeclaration = variableDeclaration;
|
|
53
|
+
results.rootNode = variableDeclaration;
|
|
54
|
+
} else {
|
|
55
|
+
results.rootNode = parent;
|
|
56
|
+
}
|
|
50
57
|
} else if (parent.type === 'MemberExpression' && parent.property.type === 'Identifier') {
|
|
51
58
|
if (parent.property.name === 'expect') {
|
|
59
|
+
// supertest assertions
|
|
52
60
|
const assertionCall = getParent(parent);
|
|
53
61
|
assert.ok(assertionCall && assertionCall.type === 'CallExpression');
|
|
54
62
|
results.assertions = [...(results.assertions ?? []), assertionCall.arguments as Expression[]];
|
|
55
63
|
nextCall = assertionCall;
|
|
56
64
|
} else if (parent.property.name === 'send') {
|
|
65
|
+
// request body
|
|
57
66
|
const sendRequestBodyCall = getParent(parent);
|
|
58
67
|
assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === 'CallExpression');
|
|
59
68
|
results.requestBody = sendRequestBodyCall.arguments[0] as Expression;
|
|
60
69
|
nextCall = sendRequestBodyCall;
|
|
61
70
|
} else if (parent.property.name === 'set') {
|
|
71
|
+
// request headers
|
|
62
72
|
const setRequestHeaderCall = getParent(parent);
|
|
63
73
|
assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === 'CallExpression');
|
|
64
74
|
const [name, value] = setRequestHeaderCall.arguments as [Expression, Expression];
|
|
@@ -69,97 +79,37 @@ function analyze(call: SimpleCallExpression, results: FixtureCallInformation) {
|
|
|
69
79
|
throw new Error(`Unexpected expression in fixture/supertest call ${String(parent)}`);
|
|
70
80
|
}
|
|
71
81
|
if (nextCall) {
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function replaceEndpointUrlPrefixWithBasePath(url: string) {
|
|
77
|
-
// eslint-disable-next-line no-template-curly-in-string
|
|
78
|
-
return url.replace(/`\/\w+(?<parts>-\w+)*\/v\d+\//u, '`${BASE_PATH}/');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function isValidPropertyName(name: unknown) {
|
|
82
|
-
return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function appendAssertions(expects: Expression[][], sourceCode: SourceCode, variableName: string) {
|
|
86
|
-
const assertions: string[] = [];
|
|
87
|
-
for (const expectArguments of expects) {
|
|
88
|
-
if (expectArguments.length === 1) {
|
|
89
|
-
const [assertionArgument] = expectArguments;
|
|
90
|
-
assert.ok(assertionArgument);
|
|
91
|
-
if (
|
|
92
|
-
(assertionArgument.type === 'MemberExpression' &&
|
|
93
|
-
assertionArgument.object.type === 'Identifier' &&
|
|
94
|
-
assertionArgument.object.name === 'StatusCodes') ||
|
|
95
|
-
assertionArgument.type === 'Literal'
|
|
96
|
-
) {
|
|
97
|
-
// status code assertion
|
|
98
|
-
assertions.push(`assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`);
|
|
99
|
-
} else if (assertionArgument.type === 'ArrowFunctionExpression') {
|
|
100
|
-
// callback assertion
|
|
101
|
-
assertions.push(`assert.ok(${sourceCode.getText(assertionArgument)})`);
|
|
102
|
-
} else if (assertionArgument.type === 'Identifier') {
|
|
103
|
-
// callback assertion
|
|
104
|
-
assertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${variableName}))`);
|
|
105
|
-
} else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') {
|
|
106
|
-
// body deep equal assertion
|
|
107
|
-
assertions.push(`assert.deepEqual(await ${variableName}.json(), ${sourceCode.getText(assertionArgument)})`);
|
|
108
|
-
} else {
|
|
109
|
-
throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`);
|
|
110
|
-
}
|
|
111
|
-
} else if (expectArguments.length === 2) {
|
|
112
|
-
// header assertion
|
|
113
|
-
const [headerName, headerValue] = expectArguments;
|
|
114
|
-
assert.ok(headerName && headerValue);
|
|
115
|
-
if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) {
|
|
116
|
-
assertions.push(
|
|
117
|
-
`assert.ok(${variableName}.headers.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`,
|
|
118
|
-
);
|
|
119
|
-
} else {
|
|
120
|
-
assertions.push(
|
|
121
|
-
`assert.equal(${variableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`,
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return assertions;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function getAncestor(node: Node, matchType: string, quitType: string) {
|
|
130
|
-
const parent = getParent(node);
|
|
131
|
-
if (!parent || parent.type === quitType) {
|
|
132
|
-
return undefined;
|
|
133
|
-
} else if (parent.type === matchType) {
|
|
134
|
-
return parent;
|
|
82
|
+
analyzeFixtureCall(nextCall, results);
|
|
135
83
|
}
|
|
136
|
-
return getAncestor(parent, matchType, quitType);
|
|
137
84
|
}
|
|
138
85
|
|
|
139
|
-
|
|
86
|
+
// analyze response related variables and their references0
|
|
87
|
+
function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, scopeManager: Scope.ScopeManager) {
|
|
140
88
|
const results: {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
89
|
+
variable?: Scope.Variable;
|
|
90
|
+
bodyReferences: MemberExpression[];
|
|
91
|
+
headersReferences: MemberExpression[];
|
|
92
|
+
statusReferences: MemberExpression[];
|
|
93
|
+
spreadBodyVariable?: Scope.Variable;
|
|
94
|
+
spreadHeadersVariable?: Scope.Variable;
|
|
146
95
|
} = {
|
|
147
|
-
|
|
148
|
-
|
|
96
|
+
bodyReferences: [],
|
|
97
|
+
headersReferences: [],
|
|
98
|
+
statusReferences: [],
|
|
149
99
|
};
|
|
150
100
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const responseVariables = scopeManager.getDeclaredVariables(variableDeclaration);
|
|
101
|
+
if (fixtureInformation.variableDeclaration) {
|
|
102
|
+
const responseVariables = scopeManager.getDeclaredVariables(fixtureInformation.variableDeclaration);
|
|
154
103
|
for (const responseVariable of responseVariables) {
|
|
155
104
|
const identifier = responseVariable.identifiers[0];
|
|
156
105
|
assert.ok(identifier);
|
|
157
106
|
const identifierParent = getParent(identifier);
|
|
158
107
|
assert.ok(identifierParent);
|
|
159
108
|
if (identifierParent.type === 'VariableDeclarator') {
|
|
160
|
-
//
|
|
161
|
-
results.
|
|
162
|
-
|
|
109
|
+
// e.g. const response = ...
|
|
110
|
+
results.variable = responseVariable;
|
|
111
|
+
// e.g. response.body
|
|
112
|
+
results.bodyReferences = responseVariable.references
|
|
163
113
|
.map((responseBodyReference) => getParent(responseBodyReference.identifier))
|
|
164
114
|
.filter(
|
|
165
115
|
(node): node is MemberExpression =>
|
|
@@ -169,7 +119,8 @@ function analyzeReferences(fixtureCall: AwaitExpression | ReturnStatement, scope
|
|
|
169
119
|
node.property.type === 'Identifier' &&
|
|
170
120
|
node.property.name === 'body',
|
|
171
121
|
);
|
|
172
|
-
|
|
122
|
+
// e.g. response.headers / response.header / response.get()
|
|
123
|
+
results.headersReferences = responseVariable.references
|
|
173
124
|
.map((responseHeadersReference) => getParent(responseHeadersReference.identifier))
|
|
174
125
|
.filter(
|
|
175
126
|
(node): node is MemberExpression =>
|
|
@@ -179,30 +130,102 @@ function analyzeReferences(fixtureCall: AwaitExpression | ReturnStatement, scope
|
|
|
179
130
|
node.property.type === 'Identifier' &&
|
|
180
131
|
(node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'),
|
|
181
132
|
);
|
|
133
|
+
// e.g. response.status / response.statusCode
|
|
134
|
+
results.statusReferences = responseVariable.references
|
|
135
|
+
.map((responseHeadersReference) => getParent(responseHeadersReference.identifier))
|
|
136
|
+
.filter(
|
|
137
|
+
(node): node is MemberExpression =>
|
|
138
|
+
node !== null &&
|
|
139
|
+
node !== undefined &&
|
|
140
|
+
node.type === 'MemberExpression' &&
|
|
141
|
+
node.property.type === 'Identifier' &&
|
|
142
|
+
(node.property.name === 'status' || node.property.name === 'statusCode'),
|
|
143
|
+
);
|
|
182
144
|
} else if (
|
|
145
|
+
// body reference through destruction/renaming, e.g. "const { body } = ..."
|
|
183
146
|
identifierParent.type === 'Property' &&
|
|
184
147
|
identifierParent.key.type === 'Identifier' &&
|
|
185
148
|
identifierParent.key.name === 'body'
|
|
186
149
|
) {
|
|
187
|
-
results.
|
|
150
|
+
results.spreadBodyVariable = responseVariable;
|
|
188
151
|
} else if (
|
|
152
|
+
// header reference through destruction/renaming, e.g. "const { headers } = ..."
|
|
189
153
|
identifierParent.type === 'Property' &&
|
|
190
154
|
identifierParent.key.type === 'Identifier' &&
|
|
191
155
|
identifierParent.key.name === 'headers'
|
|
192
156
|
) {
|
|
193
|
-
results.
|
|
157
|
+
results.spreadHeadersVariable = responseVariable;
|
|
158
|
+
} else {
|
|
159
|
+
throw new Error(`Unknown response variable reference: ${responseVariable.name}`);
|
|
194
160
|
}
|
|
195
161
|
}
|
|
196
162
|
}
|
|
197
163
|
return results;
|
|
198
164
|
}
|
|
199
165
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
166
|
+
// `/sample-service/v1/ping` -> `${BASE_PATH}/ping`
|
|
167
|
+
function replaceEndpointUrlPrefixWithBasePath(url: string) {
|
|
168
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
169
|
+
return url.replace(/`\/\w+(?<parts>-\w+)*\/v\d+\//u, '`${BASE_PATH}/');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isValidPropertyName(name: unknown) {
|
|
173
|
+
return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function createResponseAssertions(
|
|
177
|
+
fixtureCallInformation: FixtureCallInformation,
|
|
178
|
+
sourceCode: SourceCode,
|
|
179
|
+
variableName: string,
|
|
180
|
+
) {
|
|
181
|
+
// [TODO:] make sure status assertion is ordered as the first
|
|
182
|
+
let statusAssertion: string | undefined;
|
|
183
|
+
const nonStatusAssertions: string[] = [];
|
|
184
|
+
for (const expectArguments of fixtureCallInformation.assertions ?? []) {
|
|
185
|
+
if (expectArguments.length === 1) {
|
|
186
|
+
const [assertionArgument] = expectArguments;
|
|
187
|
+
assert.ok(assertionArgument);
|
|
188
|
+
if (
|
|
189
|
+
(assertionArgument.type === 'MemberExpression' &&
|
|
190
|
+
assertionArgument.object.type === 'Identifier' &&
|
|
191
|
+
assertionArgument.object.name === 'StatusCodes') ||
|
|
192
|
+
assertionArgument.type === 'Literal'
|
|
193
|
+
) {
|
|
194
|
+
// status code assertion
|
|
195
|
+
statusAssertion = `assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`;
|
|
196
|
+
} else if (assertionArgument.type === 'ArrowFunctionExpression') {
|
|
197
|
+
// callback assertion
|
|
198
|
+
nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)})`);
|
|
199
|
+
} else if (assertionArgument.type === 'Identifier') {
|
|
200
|
+
// callback assertion
|
|
201
|
+
nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${variableName}))`);
|
|
202
|
+
} else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') {
|
|
203
|
+
// body deep equal assertion
|
|
204
|
+
nonStatusAssertions.push(
|
|
205
|
+
`assert.deepEqual(await ${variableName}.json(), ${sourceCode.getText(assertionArgument)})`,
|
|
206
|
+
);
|
|
207
|
+
} else {
|
|
208
|
+
throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`);
|
|
209
|
+
}
|
|
210
|
+
} else if (expectArguments.length === 2) {
|
|
211
|
+
// header assertion
|
|
212
|
+
const [headerName, headerValue] = expectArguments;
|
|
213
|
+
assert.ok(headerName && headerValue);
|
|
214
|
+
if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) {
|
|
215
|
+
nonStatusAssertions.push(
|
|
216
|
+
`assert.ok(${variableName}.headers.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`,
|
|
217
|
+
);
|
|
218
|
+
} else {
|
|
219
|
+
nonStatusAssertions.push(
|
|
220
|
+
`assert.equal(${variableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
statusAssertion,
|
|
227
|
+
nonStatusAssertions,
|
|
228
|
+
};
|
|
206
229
|
}
|
|
207
230
|
|
|
208
231
|
const rule: Rule.RuleModule = {
|
|
@@ -223,136 +246,123 @@ const rule: Rule.RuleModule = {
|
|
|
223
246
|
create(context) {
|
|
224
247
|
const sourceCode = context.sourceCode;
|
|
225
248
|
const scopeManager = sourceCode.scopeManager;
|
|
226
|
-
let
|
|
249
|
+
let responseVariableCounter = 0;
|
|
227
250
|
|
|
228
251
|
return {
|
|
229
|
-
'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': (
|
|
252
|
+
'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': (
|
|
253
|
+
fixtureCall: CallExpression,
|
|
254
|
+
) => {
|
|
230
255
|
try {
|
|
231
256
|
assert.ok(fixtureCall.type === 'CallExpression');
|
|
232
|
-
const fixtureFunction = fixtureCall.callee; //
|
|
257
|
+
const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get
|
|
233
258
|
assert.ok(fixtureFunction.type === 'MemberExpression');
|
|
234
|
-
const methodNode = fixtureFunction.property; // get/put/etc.
|
|
235
|
-
assert.ok(methodNode.type === 'Identifier');
|
|
236
259
|
const indentation = getIndentation(fixtureCall, sourceCode);
|
|
237
260
|
|
|
238
|
-
const [urlArgumentNode] = fixtureCall.arguments; //
|
|
261
|
+
const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping`
|
|
239
262
|
assert.ok(urlArgumentNode !== undefined);
|
|
240
263
|
|
|
241
264
|
const fixtureCallInformation = {} as FixtureCallInformation;
|
|
242
|
-
|
|
265
|
+
analyzeFixtureCall(fixtureCall, fixtureCallInformation);
|
|
243
266
|
|
|
244
267
|
const {
|
|
245
|
-
responseVariable,
|
|
246
|
-
responseBodyReferences,
|
|
247
|
-
responseHeadersReferences,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
268
|
+
variable: responseVariable,
|
|
269
|
+
bodyReferences: responseBodyReferences,
|
|
270
|
+
headersReferences: responseHeadersReferences,
|
|
271
|
+
statusReferences: responseStatusReferences,
|
|
272
|
+
spreadBodyVariable: spreadResponseBodyVariable,
|
|
273
|
+
spreadHeadersVariable: spreadResponseHeadersVariable,
|
|
274
|
+
} = analyzeResponseReferences(fixtureCallInformation, scopeManager);
|
|
275
|
+
|
|
276
|
+
// convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping`
|
|
277
|
+
const originalUrlArgumentText = sourceCode.getText(urlArgumentNode);
|
|
278
|
+
const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText);
|
|
279
|
+
|
|
280
|
+
// fetch request argument
|
|
281
|
+
const methodNode = fixtureFunction.property; // get/put/etc.
|
|
282
|
+
assert.ok(methodNode.type === 'Identifier');
|
|
283
|
+
const fetchRequestArgumentLines = [
|
|
284
|
+
'{',
|
|
285
|
+
` method: '${methodNode.name.toUpperCase()}',`,
|
|
286
|
+
...(fixtureCallInformation.requestBody
|
|
287
|
+
? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`]
|
|
288
|
+
: []),
|
|
289
|
+
...(fixtureCallInformation.requestHeaders
|
|
290
|
+
? [
|
|
291
|
+
` headers: {`,
|
|
292
|
+
...fixtureCallInformation.requestHeaders.map(
|
|
293
|
+
({ name, value }) =>
|
|
294
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals
|
|
295
|
+
` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`,
|
|
296
|
+
),
|
|
297
|
+
` },`,
|
|
298
|
+
]
|
|
299
|
+
: []),
|
|
300
|
+
'}',
|
|
301
|
+
].join(`\n${indentation}`);
|
|
302
|
+
|
|
303
|
+
let responseVariableNameToUse: string;
|
|
253
304
|
if (responseVariable === undefined) {
|
|
254
|
-
|
|
255
|
-
|
|
305
|
+
responseVariableNameToUse = `response${responseVariableCounter === 0 ? '' : responseVariableCounter.toString()}`;
|
|
306
|
+
responseVariableCounter++;
|
|
256
307
|
} else {
|
|
257
|
-
|
|
258
|
-
variableNameToUse = responseVariable.name;
|
|
308
|
+
responseVariableNameToUse = responseVariable.name;
|
|
259
309
|
}
|
|
260
310
|
|
|
261
|
-
|
|
262
|
-
const fixtureApiCallText = sourceCode.getText(fixtureCall); // e.g. "fixture.api.get(`/smartdata/v1/ping`)""
|
|
263
|
-
const fixtureMethodText = sourceCode.getText(fixtureFunction); // e.g. "fixture.api.get"
|
|
264
|
-
|
|
265
|
-
const fetchStatementStart =
|
|
266
|
-
fixtureCallInformation.root.type === 'ReturnStatement' && fixtureCallInformation.assertions === undefined
|
|
267
|
-
? 'return'
|
|
268
|
-
: 'await';
|
|
269
|
-
let replacedText = fixtureApiCallText.replace(fixtureMethodText, `${fetchStatementStart} fetch`);
|
|
270
|
-
|
|
271
|
-
// convert `/smartdata/v1/ping` to `${BASE_PATH}/ping`
|
|
272
|
-
const fixtureArgumentText = sourceCode.getText(urlArgumentNode); // text - e.g. `/smartdata/v1/ping`
|
|
273
|
-
let fetchArgumentText = replaceEndpointUrlPrefixWithBasePath(fixtureArgumentText); // test - e.g. `${BASE_PATH}/ping`
|
|
274
|
-
|
|
275
|
-
// add request argument if deeded
|
|
276
|
-
if (
|
|
277
|
-
methodNode.name !== 'get' ||
|
|
278
|
-
fixtureCallInformation.requestBody !== undefined ||
|
|
279
|
-
fixtureCallInformation.requestHeaders !== undefined
|
|
280
|
-
) {
|
|
281
|
-
fetchArgumentText += [
|
|
282
|
-
', {',
|
|
283
|
-
` method: '${methodNode.name.toUpperCase()}',`,
|
|
284
|
-
...(fixtureCallInformation.requestBody
|
|
285
|
-
? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`]
|
|
286
|
-
: []),
|
|
287
|
-
...(fixtureCallInformation.requestHeaders
|
|
288
|
-
? [
|
|
289
|
-
` headers: {`,
|
|
290
|
-
...fixtureCallInformation.requestHeaders.map(
|
|
291
|
-
({ name, value }) =>
|
|
292
|
-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals
|
|
293
|
-
` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`,
|
|
294
|
-
),
|
|
295
|
-
` },`,
|
|
296
|
-
]
|
|
297
|
-
: []),
|
|
298
|
-
'}',
|
|
299
|
-
].join(`\n${indentation}`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
replacedText = replacedText.replace(fixtureArgumentText, fetchArgumentText);
|
|
303
|
-
|
|
304
|
-
const needVariableRedefine =
|
|
311
|
+
const needResponseVariableRedefine =
|
|
305
312
|
spreadResponseBodyVariable !== undefined ||
|
|
306
|
-
(responseVariable === undefined && fixtureCallInformation.assertions
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
]
|
|
318
|
-
|
|
313
|
+
(responseVariable === undefined && fixtureCallInformation.assertions !== undefined);
|
|
314
|
+
|
|
315
|
+
const responseBodyHeadersVariableRedefineLines = needResponseVariableRedefine
|
|
316
|
+
? [
|
|
317
|
+
...(spreadResponseBodyVariable
|
|
318
|
+
? [`const ${spreadResponseBodyVariable.name} = await ${responseVariableNameToUse}.json()`]
|
|
319
|
+
: []),
|
|
320
|
+
...(spreadResponseHeadersVariable
|
|
321
|
+
? [`const ${spreadResponseHeadersVariable.name} = ${responseVariableNameToUse}.headers`]
|
|
322
|
+
: []),
|
|
323
|
+
]
|
|
324
|
+
: [];
|
|
325
|
+
|
|
326
|
+
const { statusAssertion, nonStatusAssertions } = createResponseAssertions(
|
|
327
|
+
fixtureCallInformation,
|
|
328
|
+
sourceCode,
|
|
329
|
+
responseVariableNameToUse,
|
|
330
|
+
);
|
|
319
331
|
|
|
320
|
-
if
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
332
|
+
// add variable declaration if needed
|
|
333
|
+
const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`;
|
|
334
|
+
const fetchStatementText = !needResponseVariableRedefine
|
|
335
|
+
? fetchCallText
|
|
336
|
+
: `const ${responseVariableNameToUse} = await ${fetchCallText}`;
|
|
337
|
+
|
|
338
|
+
const nodeToReplace = needResponseVariableRedefine
|
|
339
|
+
? fixtureCallInformation.rootNode
|
|
340
|
+
: fixtureCallInformation.fixtureNode;
|
|
341
|
+
const appendingAssignmentAndAssertionText = [
|
|
342
|
+
'',
|
|
343
|
+
...(statusAssertion !== undefined ? [statusAssertion] : []),
|
|
344
|
+
...responseBodyHeadersVariableRedefineLines,
|
|
345
|
+
...nonStatusAssertions,
|
|
346
|
+
].join(`;\n${indentation}`);
|
|
331
347
|
|
|
332
348
|
context.report({
|
|
333
349
|
node: fixtureCall,
|
|
334
350
|
messageId: 'preferNativeFetch',
|
|
335
351
|
*fix(fixer) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
} else if (fixtureCallInformation.assertions !== undefined && responseVariable === undefined) {
|
|
344
|
-
replacementRootNode =
|
|
345
|
-
getAncestor(fixtureCallInformation.root, 'VariableDeclaration', 'FunctionDeclaration') ??
|
|
346
|
-
fixtureCallInformation.root;
|
|
347
|
-
}
|
|
348
|
-
yield fixer.replaceText(replacementRootNode, replacedText);
|
|
352
|
+
yield fixer.replaceText(nodeToReplace, fetchStatementText);
|
|
353
|
+
|
|
354
|
+
const needEndingSemiColon = sourceCode.getText(nodeToReplace).endsWith(';');
|
|
355
|
+
yield fixer.insertTextAfter(
|
|
356
|
+
nodeToReplace,
|
|
357
|
+
needEndingSemiColon ? `${appendingAssignmentAndAssertionText};` : appendingAssignmentAndAssertionText,
|
|
358
|
+
);
|
|
349
359
|
|
|
350
|
-
// handle response body
|
|
360
|
+
// handle response body references
|
|
351
361
|
for (const responseBodyReference of responseBodyReferences) {
|
|
352
|
-
yield fixer.replaceText(responseBodyReference, `await ${
|
|
362
|
+
yield fixer.replaceText(responseBodyReference, `await ${responseVariableNameToUse}.json()`);
|
|
353
363
|
}
|
|
354
364
|
|
|
355
|
-
// handle response headers
|
|
365
|
+
// handle response headers references
|
|
356
366
|
for (const responseHeadersReference of responseHeadersReferences) {
|
|
357
367
|
const parent = getParent(responseHeadersReference);
|
|
358
368
|
assert.ok(parent);
|
|
@@ -367,34 +377,29 @@ const rule: Rule.RuleModule = {
|
|
|
367
377
|
headerName = sourceCode.getText(headerNameNode);
|
|
368
378
|
}
|
|
369
379
|
assert.ok(headerName);
|
|
370
|
-
yield fixer.replaceText(parent, `${
|
|
380
|
+
yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// convert response.statusCode to response.status
|
|
384
|
+
for (const responseStatusReference of responseStatusReferences) {
|
|
385
|
+
if (
|
|
386
|
+
responseStatusReference.property.type === 'Identifier' &&
|
|
387
|
+
responseStatusReference.property.name === 'statusCode'
|
|
388
|
+
) {
|
|
389
|
+
yield fixer.replaceText(responseStatusReference.property, `status`);
|
|
390
|
+
}
|
|
371
391
|
}
|
|
372
392
|
|
|
373
393
|
// handle direct return without await
|
|
374
394
|
if (
|
|
375
|
-
fixtureCallInformation.
|
|
395
|
+
fixtureCallInformation.rootNode.type === 'ReturnStatement' &&
|
|
376
396
|
fixtureCallInformation.assertions !== undefined
|
|
377
397
|
) {
|
|
378
398
|
yield fixer.insertTextAfter(
|
|
379
|
-
fixtureCallInformation.
|
|
380
|
-
|
|
399
|
+
fixtureCallInformation.rootNode,
|
|
400
|
+
`\n${indentation}return ${responseVariableNameToUse};`,
|
|
381
401
|
);
|
|
382
402
|
}
|
|
383
|
-
|
|
384
|
-
// convert statusCode to status
|
|
385
|
-
function* statusCodeReplacer(reference: Scope.Reference) {
|
|
386
|
-
const parent = getParent(reference.identifier);
|
|
387
|
-
if (
|
|
388
|
-
parent?.type === 'MemberExpression' &&
|
|
389
|
-
parent.property.type === 'Identifier' &&
|
|
390
|
-
parent.property.name === 'statusCode'
|
|
391
|
-
) {
|
|
392
|
-
yield fixer.replaceText(parent.property, 'status');
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
for (const reference of responseVariable?.references ?? []) {
|
|
396
|
-
yield* statusCodeReplacer(reference);
|
|
397
|
-
}
|
|
398
403
|
},
|
|
399
404
|
});
|
|
400
405
|
} catch (error) {
|
|
@@ -411,4 +416,5 @@ const rule: Rule.RuleModule = {
|
|
|
411
416
|
};
|
|
412
417
|
},
|
|
413
418
|
};
|
|
419
|
+
|
|
414
420
|
export default rule;
|