@tiledesk/tiledesk-server 2.13.26 → 2.13.28
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/CHANGELOG.md +11 -0
- package/app.js +29 -5
- package/package.json +4 -4
- package/routes/kb.js +7 -5
- package/routes/webhook.js +7 -0
- package/services/RateManager.js +116 -0
- package/services/requestService.js +4 -1
- package/services/webhookService.js +1 -1
- package/test/webhookRoute.js +81 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
🚀 IN PRODUCTION 🚀
|
|
6
6
|
(https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
|
|
7
7
|
|
|
8
|
+
# 2.13.31
|
|
9
|
+
- Added default context for general LLM
|
|
10
|
+
- Updated tybot-connector to 2.0.35
|
|
11
|
+
|
|
12
|
+
# 2.13.29
|
|
13
|
+
- Minor improvements
|
|
14
|
+
|
|
15
|
+
# 2.13.27
|
|
16
|
+
- Added rate manager for webhook call
|
|
17
|
+
- Increased json body limit for /webhook endpoint
|
|
18
|
+
|
|
8
19
|
# 2.13.26
|
|
9
20
|
- Fixed bug: LLM preview not working
|
|
10
21
|
|
package/app.js
CHANGED
|
@@ -217,10 +217,14 @@ var BanUserNotifier = require('./services/banUserNotifier');
|
|
|
217
217
|
BanUserNotifier.listen();
|
|
218
218
|
const { ChatbotService } = require('./services/chatbotService');
|
|
219
219
|
const { QuoteManager } = require('./services/QuoteManager');
|
|
220
|
+
const RateManager = require('./services/RateManager');
|
|
220
221
|
|
|
221
222
|
let qm = new QuoteManager({ tdCache: tdCache });
|
|
222
223
|
qm.start();
|
|
223
224
|
|
|
225
|
+
let rm = new RateManager({ tdCache: tdCache });
|
|
226
|
+
|
|
227
|
+
|
|
224
228
|
var modulesManager = undefined;
|
|
225
229
|
try {
|
|
226
230
|
modulesManager = require('./services/modulesManager');
|
|
@@ -255,7 +259,7 @@ app.set('view engine', 'jade');
|
|
|
255
259
|
app.set('chatbot_service', new ChatbotService())
|
|
256
260
|
app.set('redis_client', tdCache);
|
|
257
261
|
app.set('quote_manager', qm);
|
|
258
|
-
|
|
262
|
+
app.set('rate_manager', rm);
|
|
259
263
|
|
|
260
264
|
// TODO DELETE IT IN THE NEXT RELEASE
|
|
261
265
|
if (process.env.ENABLE_ALTERNATIVE_CORS_MIDDLEWARE === "true") {
|
|
@@ -287,6 +291,13 @@ if (process.env.ENABLE_ALTERNATIVE_CORS_MIDDLEWARE === "true") {
|
|
|
287
291
|
const JSON_BODY_LIMIT = process.env.JSON_BODY_LIMIT || '500KB';
|
|
288
292
|
winston.debug("JSON_BODY_LIMIT : " + JSON_BODY_LIMIT);
|
|
289
293
|
|
|
294
|
+
const WEBHOOK_BODY_LIMIT = process.env.WEBHOOK_BODY_LIMIT || '5mb';
|
|
295
|
+
winston.debug("WEBHOOK_BODY_LIMIT : " + WEBHOOK_BODY_LIMIT);
|
|
296
|
+
|
|
297
|
+
const webhookParser = bodyParser.json({ limit: WEBHOOK_BODY_LIMIT });
|
|
298
|
+
|
|
299
|
+
app.use('/webhook', webhookParser, webhook);
|
|
300
|
+
|
|
290
301
|
app.use(bodyParser.json({limit: JSON_BODY_LIMIT,
|
|
291
302
|
verify: function (req, res, buf) {
|
|
292
303
|
// var url = req.originalUrl;
|
|
@@ -388,7 +399,7 @@ app.options('*', cors());
|
|
|
388
399
|
|
|
389
400
|
|
|
390
401
|
// });
|
|
391
|
-
|
|
402
|
+
console.log("MAX_UPLOAD_FILE_SIZE: ", process.env.MAX_UPLOAD_FILE_SIZE);
|
|
392
403
|
|
|
393
404
|
|
|
394
405
|
if (process.env.ROUTELOGGER_ENABLED==="true") {
|
|
@@ -517,7 +528,7 @@ app.use('/users_util', usersUtil);
|
|
|
517
528
|
app.use('/logs', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken], logs);
|
|
518
529
|
app.use('/requests_util', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken], requestUtilRoot);
|
|
519
530
|
|
|
520
|
-
app.use('/webhook', webhook);
|
|
531
|
+
//app.use('/webhook', webhook); // moved on top before body parser middleware
|
|
521
532
|
|
|
522
533
|
// TODO security issues
|
|
523
534
|
if (process.env.DISABLE_TRANSCRIPT_VIEW_PAGE ) {
|
|
@@ -674,13 +685,26 @@ app.use((err, req, res, next) => {
|
|
|
674
685
|
return res.status(401).json({ err: "error ip filter" });
|
|
675
686
|
}
|
|
676
687
|
|
|
688
|
+
const realIp = req.headers['x-forwarded-for']?.split(',')[0] || req.headers['x-real-ip'] || req.ip;
|
|
689
|
+
|
|
677
690
|
//emitted by multer when the file is too big
|
|
678
691
|
if (err.code === "LIMIT_FILE_SIZE") {
|
|
679
692
|
winston.debug("LIMIT_FILE_SIZE");
|
|
693
|
+
winston.warn(`LIMIT_FILE_SIZE on ${req.originalUrl}`, {
|
|
694
|
+
limit: process.env.MAX_UPLOAD_FILE_SIZE,
|
|
695
|
+
ip: req.ip,
|
|
696
|
+
realIp: realIp
|
|
697
|
+
});
|
|
680
698
|
return res.status(413).json({ err: "Content Too Large", limit_file_size: process.env.MAX_UPLOAD_FILE_SIZE });
|
|
681
|
-
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (err.type === "entity.too.large" || err.name === "PayloadTooLargeError") {
|
|
702
|
+
winston.warn("Payload too large", { expected: err.expected, limit: err.limit, length: err.length });
|
|
703
|
+
return res.status(413).json({ err: "Request entity too large", limit: err.limit});
|
|
704
|
+
}
|
|
705
|
+
|
|
682
706
|
|
|
683
|
-
winston.error("General error
|
|
707
|
+
winston.error("General error: ", err);
|
|
684
708
|
return res.status(500).json({ err: "error" });
|
|
685
709
|
});
|
|
686
710
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiledesk/tiledesk-server",
|
|
3
3
|
"description": "The Tiledesk server module",
|
|
4
|
-
"version": "2.13.
|
|
4
|
+
"version": "2.13.28",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"start": "node ./bin/www",
|
|
7
7
|
"pretest": "mongodb-runner start",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"@tiledesk/tiledesk-rasa-connector": "^1.0.10",
|
|
50
50
|
"@tiledesk/tiledesk-sms-connector": "^0.1.11",
|
|
51
51
|
"@tiledesk/tiledesk-telegram-connector": "^0.1.14",
|
|
52
|
-
"@tiledesk/tiledesk-tybot-connector": "^2.0.
|
|
52
|
+
"@tiledesk/tiledesk-tybot-connector": "^2.0.35",
|
|
53
53
|
"@tiledesk/tiledesk-voice-twilio-connector": "^0.1.22",
|
|
54
|
-
"@tiledesk/tiledesk-vxml-connector": "^0.1.
|
|
55
|
-
"@tiledesk/tiledesk-whatsapp-connector": "1.0.
|
|
54
|
+
"@tiledesk/tiledesk-vxml-connector": "^0.1.87",
|
|
55
|
+
"@tiledesk/tiledesk-whatsapp-connector": "1.0.11",
|
|
56
56
|
"@tiledesk/tiledesk-whatsapp-jobworker": "^0.0.13",
|
|
57
57
|
"amqplib": "^0.5.5",
|
|
58
58
|
"app-root-path": "^3.0.0",
|
package/routes/kb.js
CHANGED
|
@@ -103,7 +103,8 @@ let contexts = {
|
|
|
103
103
|
"gpt-4o-mini": "You are an helpful assistant for question-answering tasks. Follow these steps carefully:\n1. Answer in the same language of the user question, regardless of the retrieved context language\n2. Use ONLY the pieces of the retrieved context to answer the question.\n3. If the retrieved context does not contain sufficient information to generate an accurate and informative answer, return <NOANS>\n\n==Retrieved context start==\n{context}\n==Retrieved context end==",
|
|
104
104
|
"gpt-4.1": "You are an helpful assistant for question-answering tasks. Follow these steps carefully:\n1. Answer in the same language of the user question, regardless of the retrieved context language\n2. Use ONLY the pieces of the retrieved context to answer the question.\n3. If the retrieved context does not contain sufficient information to generate an accurate and informative answer, append <NOANS> at the end of the answer\n\n==Retrieved context start==\n{context}\n==Retrieved context end==",
|
|
105
105
|
"gpt-4.1-mini": "You are an helpful assistant for question-answering tasks. Follow these steps carefully:\n1. Answer in the same language of the user question, regardless of the retrieved context language\n2. Use ONLY the pieces of the retrieved context to answer the question.\n3. If the retrieved context does not contain sufficient information to generate an accurate and informative answer, append <NOANS> at the end of the answer\n\n==Retrieved context start==\n{context}\n==Retrieved context end==",
|
|
106
|
-
"gpt-4.1-nano": "You are an helpful assistant for question-answering tasks. Follow these steps carefully:\n1. Answer in the same language of the user question, regardless of the retrieved context language\n2. Use ONLY the pieces of the retrieved context to answer the question.\n3. If the retrieved context does not contain sufficient information to generate an accurate and informative answer, append <NOANS> at the end of the answer\n\n==Retrieved context start==\n{context}\n==Retrieved context end=="
|
|
106
|
+
"gpt-4.1-nano": "You are an helpful assistant for question-answering tasks. Follow these steps carefully:\n1. Answer in the same language of the user question, regardless of the retrieved context language\n2. Use ONLY the pieces of the retrieved context to answer the question.\n3. If the retrieved context does not contain sufficient information to generate an accurate and informative answer, append <NOANS> at the end of the answer\n\n==Retrieved context start==\n{context}\n==Retrieved context end==",
|
|
107
|
+
"general": "You are an helpful assistant for question-answering tasks. Follow these steps carefully:\n1. Answer in the same language of the user question, regardless of the retrieved context language\n2. Use ONLY the pieces of the retrieved context to answer the question.\n3. If the retrieved context does not contain sufficient information to generate an accurate and informative answer, append <NOANS> at the end of the answer\n\n==Retrieved context start==\n{context}\n==Retrieved context end=="
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
/**
|
|
@@ -348,10 +349,11 @@ router.post('/qa', async (req, res) => {
|
|
|
348
349
|
|
|
349
350
|
// Check if "Advanced Mode" is active. In such case the default_context must be not appended
|
|
350
351
|
if (!data.advancedPrompt) {
|
|
352
|
+
const contextTemplate = contexts[data.model] || contexts["general"];
|
|
351
353
|
if (data.system_context) {
|
|
352
|
-
data.system_context = data.system_context + " \n" +
|
|
354
|
+
data.system_context = data.system_context + " \n" + contextTemplate;
|
|
353
355
|
} else {
|
|
354
|
-
data.system_context =
|
|
356
|
+
data.system_context = contextTemplate;
|
|
355
357
|
}
|
|
356
358
|
}
|
|
357
359
|
|
|
@@ -376,12 +378,12 @@ router.post('/qa', async (req, res) => {
|
|
|
376
378
|
|
|
377
379
|
if (data.llm === 'vllm') {
|
|
378
380
|
if (!vllm_integration.value.url) {
|
|
379
|
-
return res.status(422).send({ success: false, error: "Server url for
|
|
381
|
+
return res.status(422).send({ success: false, error: "Server url for vllm is empty or invalid"})
|
|
380
382
|
}
|
|
381
383
|
data.model = {
|
|
382
384
|
name: data.model,
|
|
383
385
|
url: vllm_integration.value.url,
|
|
384
|
-
provider: '
|
|
386
|
+
provider: 'vllm'
|
|
385
387
|
}
|
|
386
388
|
data.stream = false;
|
|
387
389
|
}
|
package/routes/webhook.js
CHANGED
|
@@ -209,6 +209,13 @@ router.all('/:webhook_id', async (req, res) => {
|
|
|
209
209
|
return res.status(422).send({ success: false, error: "Webhook " + webhook_id + " is currently turned off"})
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
const rate_manager = req.app.get("rate_manager");
|
|
213
|
+
const allowed = await rate_manager.canExecute(webhook.id_project, null, 'webhook');
|
|
214
|
+
if (!allowed) {
|
|
215
|
+
winston.warn("Webhook rate limit exceeded for project " + webhook.id_project)
|
|
216
|
+
return res.status(429).send({ message: "Rate limit exceeded"});
|
|
217
|
+
}
|
|
218
|
+
|
|
212
219
|
payload.request_id = "automation-request-" + webhook.id_project + "-" + new ObjectId() + "-" + webhook_id;
|
|
213
220
|
|
|
214
221
|
// To delete - Start
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const project = require("../models/project");
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RateManager {
|
|
5
|
+
|
|
6
|
+
constructor(config) {
|
|
7
|
+
|
|
8
|
+
if (!config) {
|
|
9
|
+
throw new Error('config is mandatory')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!config.tdCache) {
|
|
13
|
+
throw new Error('config.tdCache is mandatory')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.tdCache = config.tdCache;
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
// Default rates
|
|
20
|
+
this.defaultRates = {
|
|
21
|
+
webhook: {
|
|
22
|
+
capacity: parseInt(process.env.BUCKET_WH_CAPACITY) || 10,
|
|
23
|
+
refill_rate: (parseInt(process.env.BUCKET_WH_REFILL_RATE_PER_MIN) || 10) / 60
|
|
24
|
+
},
|
|
25
|
+
message: {
|
|
26
|
+
capacity: parseInt(process.env.BUCKET_MSG_CAPACITY) || 100,
|
|
27
|
+
refill_rate: (parseInt(process.env.BUCKET_MSG_REFILL_RATE_PER_MIN) || 100) / 60
|
|
28
|
+
},
|
|
29
|
+
block: {
|
|
30
|
+
capacity: parseInt(process.env.BUCKET_BLK_CAPACITY) || 100,
|
|
31
|
+
refill_rate: (parseInt(process.env.BUCKET_BLK_REFILL_RATE_PER_MIN) || 100) / 60
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async canExecute(projectOrId, id, type) {
|
|
37
|
+
|
|
38
|
+
let project = projectOrId;
|
|
39
|
+
if (typeof projectOrId === 'string') {
|
|
40
|
+
project = await this.loadProject(projectOrId);
|
|
41
|
+
id = projectOrId;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const config = await this.getRateConfig(project, type);
|
|
45
|
+
const key = `bucket:${type}:${id}`
|
|
46
|
+
|
|
47
|
+
let bucket = await this.getBucket(key, type, project);
|
|
48
|
+
let current_tokens = bucket.tokens;
|
|
49
|
+
let elapsed = (new Date() - new Date(bucket.timestamp)) / 1000;
|
|
50
|
+
let tokens = Math.min(config.capacity, current_tokens + (elapsed * config.refill_rate));
|
|
51
|
+
|
|
52
|
+
if (tokens > 0) {
|
|
53
|
+
tokens -= 1;
|
|
54
|
+
bucket.tokens = tokens;
|
|
55
|
+
bucket.timestamp = new Date();
|
|
56
|
+
this.setBucket(key, bucket)
|
|
57
|
+
return true;
|
|
58
|
+
} else {
|
|
59
|
+
bucket.timestamp = new Date();
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getRateConfig(project, type) {
|
|
65
|
+
|
|
66
|
+
const baseConfig = this.defaultRates[type];
|
|
67
|
+
if (!project) {
|
|
68
|
+
return baseConfig;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const custom = project?.profile?.customization?.rates?.[type];
|
|
72
|
+
if (!custom) {
|
|
73
|
+
return baseConfig;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
...baseConfig,
|
|
78
|
+
...custom
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async setBucket(key, bucket) {
|
|
83
|
+
const bucket_string = JSON.stringify(bucket);
|
|
84
|
+
await this.tdCache.set(key, bucket_string, { EX: 600 });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getBucket(key, type, project) {
|
|
88
|
+
let bucket = await this.tdCache.get(key);
|
|
89
|
+
if (bucket) {
|
|
90
|
+
return JSON.parse(bucket);
|
|
91
|
+
}
|
|
92
|
+
bucket = await this.createBucket(type, project);
|
|
93
|
+
return bucket;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async createBucket(type, project) {
|
|
97
|
+
const config = await this.getRateConfig(project, type)
|
|
98
|
+
return {
|
|
99
|
+
tokens: config.capacity,
|
|
100
|
+
timestamp: new Date()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async loadProject(id_project) {
|
|
105
|
+
// Hint: implement redis cache also for project
|
|
106
|
+
try {
|
|
107
|
+
return project.findById(id_project);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
winston.error("(RateManager) Error getting project ", err);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = RateManager;
|
|
@@ -2855,7 +2855,10 @@ class RequestService {
|
|
|
2855
2855
|
winston.debug("[RequestService] response: ", response);
|
|
2856
2856
|
resolve(response.data);
|
|
2857
2857
|
}).catch((err) => {
|
|
2858
|
-
winston.error("get request parameter error:
|
|
2858
|
+
winston.error("get request parameter error:", {
|
|
2859
|
+
message: err.message,
|
|
2860
|
+
data: err.response?.data
|
|
2861
|
+
});
|
|
2859
2862
|
reject(err);
|
|
2860
2863
|
})
|
|
2861
2864
|
})
|
|
@@ -66,7 +66,7 @@ class WebhookService {
|
|
|
66
66
|
await httpUtil.post(url, payload).then((response) => {
|
|
67
67
|
resolve(response.data);
|
|
68
68
|
}).catch((err) => {
|
|
69
|
-
winston.error("Error calling webhook on post. Status " + err?.status + " " + err?.statusText + JSON.stringify(err?.response?.data));
|
|
69
|
+
winston.error("Error calling webhook on post. Status " + err?.status + " StatusText " + err?.statusText + " Data " + JSON.stringify(err?.response?.data));
|
|
70
70
|
reject(err);
|
|
71
71
|
})
|
|
72
72
|
|
package/test/webhookRoute.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
//During the test the env variable is set to test
|
|
2
2
|
process.env.NODE_ENV = 'test';
|
|
3
3
|
process.env.LOG_LEVEL = 'error';
|
|
4
|
+
process.env.BUCKET_WH_CAPACITY = 2
|
|
5
|
+
process.env.BUCKET_WH_REFILL_RATE = 2 / 60;
|
|
4
6
|
|
|
5
7
|
var projectService = require('../services/projectService');
|
|
6
8
|
var userService = require('../services/userService');
|
|
@@ -617,4 +619,83 @@ describe('WebhookRoute', () => {
|
|
|
617
619
|
});
|
|
618
620
|
});
|
|
619
621
|
})
|
|
622
|
+
|
|
623
|
+
it('webhook-rate-limit', (done) => {
|
|
624
|
+
|
|
625
|
+
var email = "test-signup-" + Date.now() + "@email.com";
|
|
626
|
+
var pwd = "pwd";
|
|
627
|
+
|
|
628
|
+
userService.signup(email, pwd, "Test Firstname", "Test lastname").then(function (savedUser) {
|
|
629
|
+
projectService.create("test-webhook-create", savedUser._id).then(function (savedProject) {
|
|
630
|
+
|
|
631
|
+
chai.request(server)
|
|
632
|
+
.post('/' + savedProject._id + '/faq_kb')
|
|
633
|
+
.auth(email, pwd)
|
|
634
|
+
.send({ name: "testbot", type: "tilebot", subtype: "webhook", language: "en", template: "blank" })
|
|
635
|
+
.end((err, res) => {
|
|
636
|
+
|
|
637
|
+
if (err) { console.error("err: ", err); }
|
|
638
|
+
if (log) { console.log("res.body", res.body); }
|
|
639
|
+
|
|
640
|
+
res.should.have.status(200);
|
|
641
|
+
res.body.should.be.a('object');
|
|
642
|
+
|
|
643
|
+
let chatbot_id = res.body._id;
|
|
644
|
+
let webhook_intent_id = "3bfda939-ff76-4762-bbe0-fc0f0dc4c777"
|
|
645
|
+
|
|
646
|
+
chai.request(server)
|
|
647
|
+
.post('/' + savedProject._id + '/webhooks/')
|
|
648
|
+
.auth(email, pwd)
|
|
649
|
+
.send({ chatbot_id: chatbot_id, block_id: webhook_intent_id })
|
|
650
|
+
.end((err, res) => {
|
|
651
|
+
|
|
652
|
+
if (err) { console.error("err: ", err); }
|
|
653
|
+
if (log) { console.log("res.body", res.body); }
|
|
654
|
+
|
|
655
|
+
res.should.have.status(200);
|
|
656
|
+
res.body.should.be.a('object');
|
|
657
|
+
|
|
658
|
+
let webhook_id = res.body.webhook_id;
|
|
659
|
+
|
|
660
|
+
let iterations = 4;
|
|
661
|
+
let interval = 1000;
|
|
662
|
+
async function send(i) {
|
|
663
|
+
chai.request(server)
|
|
664
|
+
.post('/webhook/' + webhook_id)
|
|
665
|
+
.auth(email, pwd)
|
|
666
|
+
.end((err, res) => {
|
|
667
|
+
|
|
668
|
+
if (err) { console.error("err: ", err); }
|
|
669
|
+
if (log) { console.log("res.body", res.body); }
|
|
670
|
+
|
|
671
|
+
if (i !== 3) {
|
|
672
|
+
res.should.have.status(200);
|
|
673
|
+
res.body.should.be.a('object');
|
|
674
|
+
expect(res.body.success).to.equal(true);
|
|
675
|
+
expect(res.body.message).to.equal("Webhook disabled in test mode");
|
|
676
|
+
} else {
|
|
677
|
+
res.should.have.status(429);
|
|
678
|
+
res.body.should.be.a('object');
|
|
679
|
+
expect(res.body.message).to.equal("Rate limit exceeded");
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
i += 1;
|
|
683
|
+
if (i < iterations) {
|
|
684
|
+
setTimeout(() => {
|
|
685
|
+
send(i)
|
|
686
|
+
}, interval);
|
|
687
|
+
} else {
|
|
688
|
+
done();
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
send(0);
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
})
|
|
620
701
|
});
|