serverless-openapi-documenter 0.0.18 → 0.0.21
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serverless-openapi-documenter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"description": "Generate OpenAPI v3 documentation and Postman Collections from your Serverless Config",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"@apidevtools/json-schema-ref-parser": "^9.0.9",
|
|
32
32
|
"chalk": "^4.1.2",
|
|
33
33
|
"js-yaml": "^4.1.0",
|
|
34
|
-
"json-schema-for-openapi": "^0.
|
|
34
|
+
"json-schema-for-openapi": "^0.2.0",
|
|
35
35
|
"oas-validator": "^5.0.8",
|
|
36
|
-
"openapi-to-postmanv2": "^
|
|
36
|
+
"openapi-to-postmanv2": "^4.1.0",
|
|
37
37
|
"swagger2openapi": "^7.0.8",
|
|
38
38
|
"uuid": "^8.3.2"
|
|
39
39
|
},
|
package/src/openAPIGenerator.js
CHANGED
|
@@ -106,7 +106,7 @@ class OpenAPIGenerator {
|
|
|
106
106
|
|
|
107
107
|
async generate() {
|
|
108
108
|
this.log(this.defaultLog, chalk.bold.underline('OpenAPI v3 Document Generation'))
|
|
109
|
-
|
|
109
|
+
this.processCliInput()
|
|
110
110
|
const generator = new DefinitionGenerator(this.serverless);
|
|
111
111
|
|
|
112
112
|
await generator.parse()
|
|
@@ -124,42 +124,22 @@ class OpenAPIGenerator {
|
|
|
124
124
|
if (valid)
|
|
125
125
|
this.log('success', 'OpenAPI v3 Documentation Successfully Generated')
|
|
126
126
|
|
|
127
|
-
if (config.postmanCollection) {
|
|
128
|
-
|
|
129
|
-
if (err) {
|
|
130
|
-
this.log('error', `ERROR: An error was thrown when generating the postman collection`)
|
|
131
|
-
throw new this.serverless.classes.Error(err)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
this.log('success', 'postman collection v2 Documentation Successfully Generated')
|
|
135
|
-
try {
|
|
136
|
-
fs.writeFileSync(config.postmanCollection, JSON.stringify(result.output[0].data))
|
|
137
|
-
this.log('success', 'postman collection v2 Documentation Successfully Written')
|
|
138
|
-
} catch (err) {
|
|
139
|
-
this.log('error', `ERROR: An error was thrown whilst writing the postman collection`)
|
|
140
|
-
throw new this.serverless.classes.Error(err)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const postmanCollection = PostmanGenerator.convert(
|
|
145
|
-
{type: 'json', data: JSON.parse(JSON.stringify(generator.openAPI))},
|
|
146
|
-
{},
|
|
147
|
-
postmanGeneration
|
|
148
|
-
)
|
|
127
|
+
if (this.config.postmanCollection) {
|
|
128
|
+
this.createPostman(generator.openAPI)
|
|
149
129
|
}
|
|
150
130
|
|
|
151
131
|
let output
|
|
152
|
-
switch (config.format.toLowerCase()) {
|
|
132
|
+
switch (this.config.format.toLowerCase()) {
|
|
153
133
|
case 'json':
|
|
154
|
-
output = JSON.stringify(generator.openAPI, null, config.indent);
|
|
134
|
+
output = JSON.stringify(generator.openAPI, null, this.config.indent);
|
|
155
135
|
break;
|
|
156
136
|
case 'yaml':
|
|
157
137
|
default:
|
|
158
|
-
output = yaml.dump(generator.openAPI, { indent: config.indent });
|
|
138
|
+
output = yaml.dump(generator.openAPI, { indent: this.config.indent });
|
|
159
139
|
break;
|
|
160
140
|
}
|
|
161
141
|
try {
|
|
162
|
-
fs.writeFileSync(config.file, output);
|
|
142
|
+
fs.writeFileSync(this.config.file, output);
|
|
163
143
|
this.log('success', 'OpenAPI v3 Documentation Successfully Written')
|
|
164
144
|
} catch (err) {
|
|
165
145
|
this.log('error', `ERROR: An error was thrown whilst writing the openAPI Documentation`)
|
|
@@ -167,6 +147,30 @@ class OpenAPIGenerator {
|
|
|
167
147
|
}
|
|
168
148
|
}
|
|
169
149
|
|
|
150
|
+
createPostman(openAPI) {
|
|
151
|
+
const postmanGeneration = (err, result) => {
|
|
152
|
+
if (err) {
|
|
153
|
+
this.log('error', `ERROR: An error was thrown when generating the postman collection`)
|
|
154
|
+
throw new this.serverless.classes.Error(err)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.log('success', 'postman collection v2 Documentation Successfully Generated')
|
|
158
|
+
try {
|
|
159
|
+
fs.writeFileSync(this.config.postmanCollection, JSON.stringify(result.output[0].data))
|
|
160
|
+
this.log('success', 'postman collection v2 Documentation Successfully Written')
|
|
161
|
+
} catch (err) {
|
|
162
|
+
this.log('error', `ERROR: An error was thrown whilst writing the postman collection`)
|
|
163
|
+
throw new this.serverless.classes.Error(err)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
PostmanGenerator.convert(
|
|
168
|
+
{type: 'json', data: JSON.parse(JSON.stringify(openAPI))},
|
|
169
|
+
{},
|
|
170
|
+
postmanGeneration
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
170
174
|
processCliInput () {
|
|
171
175
|
const config = {
|
|
172
176
|
format: 'json',
|
|
@@ -182,7 +186,6 @@ class OpenAPIGenerator {
|
|
|
182
186
|
config.postmanCollection = this.serverless.processedInput.options.postmanCollection || null
|
|
183
187
|
|
|
184
188
|
if (['yaml', 'json'].indexOf(config.format.toLowerCase()) < 0) {
|
|
185
|
-
// throw new Error('Invalid Output Format Specified - must be one of "yaml" or "json"');
|
|
186
189
|
throw new this.serverless.classes.Error('Invalid Output Format Specified - must be one of "yaml" or "json"')
|
|
187
190
|
}
|
|
188
191
|
|
|
@@ -199,7 +202,7 @@ class OpenAPIGenerator {
|
|
|
199
202
|
${config.postmanCollection ? `postman collection: ${chalk.bold.green(config.postmanCollection)}`: `\n\n`}`
|
|
200
203
|
)
|
|
201
204
|
|
|
202
|
-
|
|
205
|
+
this.config = config
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
validateDetails(validation) {
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.3",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "serverless-openapi-doc-demo",
|
|
5
|
+
"description": "This is a description of what this does",
|
|
6
|
+
"version": "1.0.0"
|
|
7
|
+
},
|
|
8
|
+
"components": {
|
|
9
|
+
"schemas": {
|
|
10
|
+
"username": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"pattern": "^[-a-z0-9_]+$"
|
|
13
|
+
},
|
|
14
|
+
"membershipType": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": [
|
|
17
|
+
"premium",
|
|
18
|
+
"standard"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"SessionId": {
|
|
22
|
+
"type": "string"
|
|
23
|
+
},
|
|
24
|
+
"PutDocumentRequest": {
|
|
25
|
+
"properties": {
|
|
26
|
+
"SomeObject": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"SomeAttribute": {
|
|
30
|
+
"type": "string"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"PutDocumentResponse": {
|
|
37
|
+
"title": "Empty Schema",
|
|
38
|
+
"type": "object"
|
|
39
|
+
},
|
|
40
|
+
"error": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {
|
|
43
|
+
"id": {
|
|
44
|
+
"description": "A unique identifier for this particular occurrence of the problem.",
|
|
45
|
+
"type": "string"
|
|
46
|
+
},
|
|
47
|
+
"links": {
|
|
48
|
+
"$ref": "#/components/schemas/links"
|
|
49
|
+
},
|
|
50
|
+
"status": {
|
|
51
|
+
"description": "The HTTP status code applicable to this problem, expressed as a string value.",
|
|
52
|
+
"type": "string"
|
|
53
|
+
},
|
|
54
|
+
"code": {
|
|
55
|
+
"description": "An application-specific error code, expressed as a string value.",
|
|
56
|
+
"type": "string"
|
|
57
|
+
},
|
|
58
|
+
"title": {
|
|
59
|
+
"description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.",
|
|
60
|
+
"type": "string"
|
|
61
|
+
},
|
|
62
|
+
"detail": {
|
|
63
|
+
"description": "A human-readable explanation specific to this occurrence of the problem.",
|
|
64
|
+
"type": "string"
|
|
65
|
+
},
|
|
66
|
+
"source": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
"pointer": {
|
|
70
|
+
"description": "A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].",
|
|
71
|
+
"type": "string"
|
|
72
|
+
},
|
|
73
|
+
"parameter": {
|
|
74
|
+
"description": "A string indicating which query parameter caused the error.",
|
|
75
|
+
"type": "string"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"meta": {
|
|
80
|
+
"$ref": "#/components/schemas/meta"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"additionalProperties": false
|
|
84
|
+
},
|
|
85
|
+
"meta": {
|
|
86
|
+
"description": "Non-standard meta-information that can not be represented as an attribute or relationship.",
|
|
87
|
+
"type": "object",
|
|
88
|
+
"additionalProperties": true
|
|
89
|
+
},
|
|
90
|
+
"links": {
|
|
91
|
+
"description": "A resource object **MAY** contain references to other resource objects (\"relationships\"). Relationships may be to-one or to-many. Relationships can be specified by including a member in a resource's links object.",
|
|
92
|
+
"type": "object",
|
|
93
|
+
"properties": {
|
|
94
|
+
"self": {
|
|
95
|
+
"description": "A `self` member, whose value is a URL for the relationship itself (a \"relationship URL\"). This URL allows the client to directly manipulate the relationship. For example, it would allow a client to remove an `author` from an `article` without deleting the people resource itself.",
|
|
96
|
+
"type": "string",
|
|
97
|
+
"format": "uri"
|
|
98
|
+
},
|
|
99
|
+
"related": {
|
|
100
|
+
"$ref": "#/components/schemas/link"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"additionalProperties": true
|
|
104
|
+
},
|
|
105
|
+
"link": {
|
|
106
|
+
"description": "A link **MUST** be represented as either: a string containing the link's URL or a link object.",
|
|
107
|
+
"oneOf": [
|
|
108
|
+
{
|
|
109
|
+
"description": "A string containing the link's URL.",
|
|
110
|
+
"type": "string",
|
|
111
|
+
"format": "uri"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"type": "object",
|
|
115
|
+
"required": [
|
|
116
|
+
"href"
|
|
117
|
+
],
|
|
118
|
+
"properties": {
|
|
119
|
+
"href": {
|
|
120
|
+
"description": "A string containing the link's URL.",
|
|
121
|
+
"type": "string",
|
|
122
|
+
"format": "uri"
|
|
123
|
+
},
|
|
124
|
+
"meta": {
|
|
125
|
+
"$ref": "#/components/schemas/meta"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
"ErrorResponse": {
|
|
132
|
+
"title": "JSON API Schema",
|
|
133
|
+
"description": "This is a schema for responses in the JSON API format. For more, see http://jsonapi.org",
|
|
134
|
+
"type": "object",
|
|
135
|
+
"required": [
|
|
136
|
+
"errors"
|
|
137
|
+
],
|
|
138
|
+
"properties": {
|
|
139
|
+
"errors": {
|
|
140
|
+
"type": "array",
|
|
141
|
+
"items": {
|
|
142
|
+
"$ref": "#/components/schemas/error"
|
|
143
|
+
},
|
|
144
|
+
"uniqueItems": true
|
|
145
|
+
},
|
|
146
|
+
"meta": {
|
|
147
|
+
"$ref": "#/components/schemas/meta"
|
|
148
|
+
},
|
|
149
|
+
"links": {
|
|
150
|
+
"$ref": "#/components/schemas/links"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"additionalProperties": false
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"paths": {
|
|
158
|
+
"/create/{username}": {
|
|
159
|
+
"post": {
|
|
160
|
+
"summary": "Create User",
|
|
161
|
+
"description": "Creates a user and then sends a generated password email",
|
|
162
|
+
"operationId": "serverless-openapi-doc-demo-dev-createUser",
|
|
163
|
+
"parameters": [
|
|
164
|
+
{
|
|
165
|
+
"name": "username",
|
|
166
|
+
"in": "path",
|
|
167
|
+
"description": "The username for a user to create",
|
|
168
|
+
"required": true,
|
|
169
|
+
"schema": {
|
|
170
|
+
"$ref": "#/components/schemas/username"
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "membershipType",
|
|
175
|
+
"in": "query",
|
|
176
|
+
"description": "The user's Membership Type",
|
|
177
|
+
"required": false,
|
|
178
|
+
"schema": {
|
|
179
|
+
"$ref": "#/components/schemas/membershipType"
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "SessionId",
|
|
184
|
+
"in": "cookie",
|
|
185
|
+
"description": "A Session ID variable",
|
|
186
|
+
"required": false,
|
|
187
|
+
"schema": {
|
|
188
|
+
"$ref": "#/components/schemas/SessionId"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
"tags": [
|
|
193
|
+
"jesus"
|
|
194
|
+
],
|
|
195
|
+
"externalDocs": {
|
|
196
|
+
"url": "https://bing.com",
|
|
197
|
+
"description": "A link to bing"
|
|
198
|
+
},
|
|
199
|
+
"requestBody": {
|
|
200
|
+
"description": "A user information object",
|
|
201
|
+
"required": false,
|
|
202
|
+
"content": {
|
|
203
|
+
"application/json": {
|
|
204
|
+
"schema": {
|
|
205
|
+
"$ref": "#/components/schemas/PutDocumentRequest"
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
"responses": {
|
|
211
|
+
"201": {
|
|
212
|
+
"description": "A user object along with generated API Keys",
|
|
213
|
+
"content": {
|
|
214
|
+
"application/json": {
|
|
215
|
+
"schema": {
|
|
216
|
+
"$ref": "#/components/schemas/PutDocumentResponse"
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
"500": {
|
|
222
|
+
"description": "An error message when creating a new user",
|
|
223
|
+
"content": {
|
|
224
|
+
"application/json": {
|
|
225
|
+
"schema": {
|
|
226
|
+
"$ref": "#/components/schemas/ErrorResponse"
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
"summary": "a function",
|
|
234
|
+
"description": "blah blah"
|
|
235
|
+
},
|
|
236
|
+
"/patch/": {
|
|
237
|
+
"patch": {
|
|
238
|
+
"summary": "Patch a User",
|
|
239
|
+
"description": "Patch details about the user",
|
|
240
|
+
"operationId": "serverless-openapi-doc-demo-dev-patchUser",
|
|
241
|
+
"parameters": [],
|
|
242
|
+
"tags": [
|
|
243
|
+
"patching"
|
|
244
|
+
],
|
|
245
|
+
"responses": {
|
|
246
|
+
"200": {
|
|
247
|
+
"description": "A user object along with generated API Keys",
|
|
248
|
+
"content": {
|
|
249
|
+
"application/json": {
|
|
250
|
+
"schema": {
|
|
251
|
+
"$ref": "#/components/schemas/PutDocumentResponse"
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
"tags": [
|
|
261
|
+
{
|
|
262
|
+
"name": "jesus",
|
|
263
|
+
"description": "jesus was a man",
|
|
264
|
+
"externalDocs": {
|
|
265
|
+
"url": "https://whitehouse.gov",
|
|
266
|
+
"description": "a link to the whitehouse"
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
"externalDocs": {
|
|
271
|
+
"url": "https://google.com",
|
|
272
|
+
"description": "A link to google"
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -1,10 +1,101 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const PostmanGenerator = require('openapi-to-postmanv2')
|
|
3
5
|
const sinon = require('sinon')
|
|
4
6
|
const expect = require('chai').expect
|
|
5
7
|
|
|
8
|
+
const validOpenAPI = require('../json/valid-openAPI.json')
|
|
9
|
+
|
|
6
10
|
const OpenAPIGenerator = require('../../src/openAPIGenerator')
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
describe('OpenAPIGenerator', () => {
|
|
13
|
+
let sls, logOutput
|
|
14
|
+
beforeEach(function() {
|
|
15
|
+
sls = {
|
|
16
|
+
version: '3.0.0',
|
|
17
|
+
variables: {
|
|
18
|
+
service: {
|
|
19
|
+
custom: {
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
configSchemaHandler: {
|
|
25
|
+
defineFunctionEventProperties: () => {},
|
|
26
|
+
defineFunctionProperties: () => {}
|
|
27
|
+
},
|
|
28
|
+
classes: {
|
|
29
|
+
Error: class ServerlessError {constructor(err) {return new Error(err)}}
|
|
30
|
+
},
|
|
31
|
+
processedInput: {
|
|
32
|
+
options: {
|
|
33
|
+
postmanCollection: 'postman.json'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
logOutput = {
|
|
39
|
+
log: {
|
|
40
|
+
notice: (str) => {},
|
|
41
|
+
error: (str) => {},
|
|
42
|
+
success: (str) => {}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
describe('createPostman', () => {
|
|
47
|
+
it('should generate a postman collection when a valid openAPI file is generated', function() {
|
|
48
|
+
const fsStub = sinon.stub(fs, 'writeFileSync').returns(true)
|
|
49
|
+
const succSpy = sinon.spy(logOutput.log, 'success')
|
|
50
|
+
const errSpy = sinon.spy(logOutput.log, 'error')
|
|
51
|
+
const openAPIGenerator = new OpenAPIGenerator(sls, {}, logOutput)
|
|
52
|
+
openAPIGenerator.processCliInput()
|
|
53
|
+
|
|
54
|
+
openAPIGenerator.createPostman(validOpenAPI)
|
|
55
|
+
|
|
56
|
+
expect(fsStub.called).to.be.true
|
|
57
|
+
expect(succSpy.calledTwice).to.be.true
|
|
58
|
+
expect(errSpy.called).to.be.false
|
|
59
|
+
fsStub.restore()
|
|
60
|
+
succSpy.restore()
|
|
61
|
+
errSpy.restore()
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should throw an error when writing a file fails', function() {
|
|
65
|
+
const errStub = sinon.stub(logOutput.log, 'error').returns('')
|
|
66
|
+
const succSpy = sinon.spy(logOutput.log, 'success')
|
|
67
|
+
const fsStub = sinon.stub(fs, 'writeFileSync').throws(new Error())
|
|
68
|
+
const openAPIGenerator = new OpenAPIGenerator(sls, {}, logOutput)
|
|
69
|
+
openAPIGenerator.processCliInput()
|
|
70
|
+
|
|
71
|
+
expect(() => {openAPIGenerator.createPostman(validOpenAPI)}).to.throw()
|
|
72
|
+
|
|
73
|
+
expect(fsStub.called).to.be.true
|
|
74
|
+
expect(errStub.called).to.be.true
|
|
75
|
+
expect(succSpy.calledOnce).to.be.true
|
|
76
|
+
expect(succSpy.calledTwice).to.be.false
|
|
77
|
+
fsStub.restore()
|
|
78
|
+
succSpy.restore()
|
|
79
|
+
errStub.restore()
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should throw an error converting an OpenAPI fails', function() {
|
|
83
|
+
const errStub = sinon.spy(logOutput.log, 'error')
|
|
84
|
+
const succSpy = sinon.spy(logOutput.log, 'success')
|
|
85
|
+
const pgStub = sinon.stub(PostmanGenerator, 'convert')
|
|
86
|
+
pgStub.yields(new Error())
|
|
87
|
+
|
|
88
|
+
const openAPIGenerator = new OpenAPIGenerator(sls, {}, logOutput)
|
|
89
|
+
openAPIGenerator.processCliInput()
|
|
90
|
+
|
|
91
|
+
expect(() => {openAPIGenerator.createPostman(validOpenAPI)}).to.throw()
|
|
92
|
+
|
|
93
|
+
expect(errStub.called).to.be.true
|
|
94
|
+
expect(succSpy.calledOnce).to.be.false
|
|
95
|
+
expect(succSpy.calledTwice).to.be.false
|
|
9
96
|
|
|
97
|
+
succSpy.restore()
|
|
98
|
+
errStub.restore()
|
|
99
|
+
});
|
|
100
|
+
});
|
|
10
101
|
});
|