From fc68e14b319ab79247fc1b4e1effe267e4c52d98 Mon Sep 17 00:00:00 2001 From: "Kees C. Bakker" Date: Tue, 30 Apr 2024 21:37:05 +0200 Subject: [PATCH 1/9] Refactored the the dependency on robot.http away. --- src/Bot.js | 2 +- .../implementations/RocketChatUploader.js | 155 +++++++++++------- src/grafana-client.js | 122 +++++++------- src/http.js | 21 --- src/service/GrafanaService.js | 8 +- 5 files changed, 163 insertions(+), 145 deletions(-) delete mode 100644 src/http.js diff --git a/src/Bot.js b/src/Bot.js index f764df2..18386b0 100644 --- a/src/Bot.js +++ b/src/Bot.js @@ -42,7 +42,7 @@ class Bot { return null; } - let client = new GrafanaClient(robot.http, robot.logger, host, apiKey); + let client = new GrafanaClient(robot.logger, host, apiKey); return new GrafanaService(client); } diff --git a/src/adapters/implementations/RocketChatUploader.js b/src/adapters/implementations/RocketChatUploader.js index 5f1dc37..eecb7b8 100644 --- a/src/adapters/implementations/RocketChatUploader.js +++ b/src/adapters/implementations/RocketChatUploader.js @@ -1,5 +1,4 @@ 'strict'; -const { post } = require('../../http'); const { Uploader } = require('../Uploader'); class RocketChatUploader extends Uploader { @@ -20,7 +19,11 @@ class RocketChatUploader extends Uploader { /** @type {string} */ this.rocketchat_url = process.env.ROCKETCHAT_URL; - if (this.rocketchat_url && !this.rocketchat_url.startsWith('http://') && !this.rocketchat_url.startsWith('https://')) { + if ( + this.rocketchat_url && + !this.rocketchat_url.startsWith('http://') && + !this.rocketchat_url.startsWith('https://') + ) { this.rocketchat_url = `http://${rocketchat_url}`; } @@ -31,6 +34,40 @@ class RocketChatUploader extends Uploader { this.logger = logger; } + /** + * Logs in to the RocketChat API using the provided credentials. + * @returns {Promise<{'X-Auth-Token': string, 'X-User-Id': string}>} A promise that resolves to the authentication headers if successful. + * @throws {Error} If authentication fails. + */ + async login() { + const authUrl = `${this.rocketchat_url}/api/v1/login`; + const authForm = { + username: this.rocketchat_user, + password: this.rocketchat_password, + }; + + let rocketchatResBodyJson = null; + + try { + rocketchatResBodyJson = await post(authUrl, authForm); + } catch (err) { + this.logger.error(err); + throw new Error('Could not authenticate.'); + } + + const { status } = rocketchatResBodyJson; + if (status === 'success') { + return { + 'X-Auth-Token': rocketchatResBodyJson.data.authToken, + 'X-User-Id': rocketchatResBodyJson.data.userId, + }; + } + + const errMsg = rocketchatResBodyJson.message; + this.logger.error(errMsg); + throw new Error(errMsg); + } + /** * Uploads the a screenshot of the dashboards. * @@ -39,69 +76,67 @@ class RocketChatUploader extends Uploader { * @param {{ body: Buffer, contentType: string}=>void} file the screenshot. * @param {string} grafanaChartLink link to the Grafana chart. */ - upload(res, title, file, grafanaChartLink) { - const authData = { - url: `${this.rocketchat_url}/api/v1/login`, - form: { - username: this.rocketchat_user, - password: this.rocketchat_password, + async upload(res, title, file, grafanaChartLink) { + let authHeaders = null; + try { + authHeaders = await this.login(); + } catch (ex) { + let msg = ex == 'Could not authenticate.' ? "invalid url, user or password/can't access rocketchat api" : ex; + res.send(`${title} - [Rocketchat auth Error - ${msg}] - ${grafanaChartLink}`); + return; + } + + // fill in the POST request. This must be www-form/multipart + // TODO: needs some extra testing! + const uploadUrl = `${this.rocketchat_url}/api/v1/rooms.upload/${res.envelope.user.roomID}`; + const uploadForm = { + msg: `${title}: ${grafanaChartLink}`, + // grafanaDashboardRequest() is the method that downloads the .png + file: { + value: file.body, + options: { + filename: `${title} ${Date()}.png`, + contentType: 'image/png', + }, }, }; - // We auth against rocketchat to obtain the auth token - post(robot, authData, async (err, rocketchatResBodyJson) => { - if (err) { - this.logger.error(err); - res.send(`${title} - [Rocketchat auth Error - invalid url, user or password/can't access rocketchat api] - ${grafanaChartLink}`); - return; - } - let errMsg; - const { status } = rocketchatResBodyJson; - if (status !== 'success') { - errMsg = rocketchatResBodyJson.message; - this.logger.error(errMsg); - res.send(`${title} - [Rocketchat auth Error - ${errMsg}] - ${grafanaChartLink}`); - return; - } - - const auth = rocketchatResBodyJson.data; - - // fill in the POST request. This must be www-form/multipart - // TODO: needs some extra testing! - const uploadData = { - url: `${this.rocketchat_url}/api/v1/rooms.upload/${res.envelope.user.roomID}`, - headers: { - 'X-Auth-Token': auth.authToken, - 'X-User-Id': auth.userId, - }, - formData: { - msg: `${title}: ${grafanaChartLink}`, - // grafanaDashboardRequest() is the method that downloads the .png - file: { - value: file.body, - options: { - filename: `${title} ${Date()}.png`, - contentType: 'image/png', - }, - }, - }, - }; + let body = null; + + try { + body = await this.post(uploadUrl, uploadForm, authHeaders); + } catch (err) { + this.logger.error(err); + res.send(`${title} - [Upload Error] - ${grafanaChartLink}`); + return; + } + + if (!body.success) { + this.logger.error(`rocketchat service error while posting data:${body.error}`); + return res.send(`${title} - [Form Error: can't upload file : ${body.error}] - ${grafanaChartLink}`); + } + } - // Try to upload the image to rocketchat else pass the link over - return post(this.robot, uploadData, (err, body) => { - // Error logging, we must also check the body response. - // It will be something like: { "success": , "error": } - if (err) { - this.logger.error(err); - return res.send(`${title} - [Upload Error] - ${grafanaChartLink}`); - } - if (!body.success) { - errMsg = body.error; - this.logger.error(`rocketchat service error while posting data:${errMsg}`); - return res.send(`${title} - [Form Error: can't upload file : ${errMsg}] - ${grafanaChartLink}`); - } - }); + /** + * Posts the data data to the specified url and returns JSON. + * @param {string} url - the URL + * @param {Record} formData - formatData + * @param {Record|null} headers - formatData + * @returns {Promise} The deserialized JSON response or an error if something went wrong. + */ + async post(url, formData, headers = null) { + const response = await fetch(url, { + method: 'POST', + headers: headers, + body: new FormData(formData), }); + + if (!response.ok) { + throw new Error('HTTP request failed'); + } + + const data = await response.json(); + return data; } } diff --git a/src/grafana-client.js b/src/grafana-client.js index e8ad877..2695a93 100644 --- a/src/grafana-client.js +++ b/src/grafana-client.js @@ -7,18 +7,11 @@ const { URL, URLSearchParams } = require('url'); class GrafanaClient { /** * Creates a new instance. - * @param {(url: string, options?: HttpOptions)=>ScopedClient} http the HTTP client. * @param {Hubot.Log} logger the logger. * @param {string} host the host. * @param {string} apiKey the api key. */ - constructor(http, logger, host, apiKey) { - /** - * The HTTP client - * @type {(url: string, options?: HttpOptions)=>ScopedClient} - */ - this.http = http; - + constructor(logger, host, apiKey) { /** * The logger. * @type {Hubot.Log} @@ -39,66 +32,71 @@ class GrafanaClient { } /** - * Creates a scoped HTTP client. - * @param {string} url The URL. - * @param {string | null} contentType Indicates if the HTTP client should post. - * @param {encoding | false} encoding Indicates if an encoding should be set. - * @returns {ScopedClient} + * Performs a GET on the Grafana API. + * Remarks: uses Hubot because of Nock testing. + * @param {string} url the url + * @returns {Promise} the response data */ - createHttpClient(url, contentType = null, encoding = false) { + async get(url) { if (!url.startsWith('http://') && !url.startsWith('https://') && !this.host) { throw new Error('No Grafana endpoint configured.'); } - // in case of a download we get a "full" URL const fullUrl = url.startsWith('http://') || url.startsWith('https://') ? url : `${this.host}/api/${url}`; - const headers = grafanaHeaders(contentType, encoding, this.apiKey); - const client = this.http(fullUrl).headers(headers); - - return client; - } - /** - * Performs a GET on the Grafana API. - * Remarks: uses Hubot because of Nock testing. - * @param {string} url the url - * @returns {Promise} - */ - async get(url) { - let client = this.createHttpClient(url); - return new Promise((resolve) => { - client.get()((err, res, body) => { - if (err) { - throw err; - } - const data = JSON.parse(body); - return resolve(data); - }); + const response = await fetch(fullUrl, { + method: 'GET', + headers: grafanaHeaders(null, false, this.apiKey), }); + + await this.throwIfNotOk(response); + + const json = await response.json(); + return json; } /** * Performs a POST call to the Grafana API. * * @param {string} url The API sub URL - * @param {Record} data The data that will be sent. - * @returns {Promise} + * @param {Record} data The data that will be sent. + * @returns {Promise} */ - post(url, data) { - const http = this.createHttpClient(url, 'application/json'); - const jsonPayload = JSON.stringify(data); - - return new Promise((resolve, reject) => { - http.post(jsonPayload)((err, res, body) => { - if (err) { - reject(err); - return; - } - - data = JSON.parse(body); - resolve(data); - }); + async post(url, data) { + const fullUrl = url.startsWith('http://') || url.startsWith('https://') ? url : `${this.host}/api/${url}`; + + const response = await fetch(fullUrl, { + method: 'POST', + headers: grafanaHeaders('application/json', false, this.apiKey), + body: JSON.stringify(data), }); + + await this.throwIfNotOk(response); + + const json = await response.json(); + return json; + } + + /** + * Ensures that the response is OK. If the response is not OK, an error is thrown. + * @param {fetch.Response} response - The response object. + * @throws {Error} If the response is not OK, an error with the response text is thrown. + */ + async throwIfNotOk(response) { + if (response.ok) { + return; + } + + if (response.headers.get('content-type') == 'application/json') { + const json = await response.json(); + + const error = new Error(json.message || 'Error while fetching data from Grafana.'); + error.data = json; + throw error; + } + + const text = await response.text(); + throw new Error(text); } /** @@ -107,18 +105,20 @@ class GrafanaClient { * @returns {Promise} */ async download(url) { - return await fetch(url, { + let response = await fetch(url, { method: 'GET', headers: grafanaHeaders(null, null, this.apiKey), - }).then(async (res) => { - const contentType = res.headers.get('content-type'); - const body = await res.arrayBuffer(); - - return { - body: Buffer.from(body), - contentType: contentType, - }; }); + + await this.throwIfNotOk(response); + + const contentType = response.headers.get('content-type'); + const body = await response.arrayBuffer(); + + return { + body: Buffer.from(body), + contentType: contentType, + }; } createGrafanaChartLink(query, uid, panel, timeSpan, variables) { diff --git a/src/http.js b/src/http.js deleted file mode 100644 index 4e8e152..0000000 --- a/src/http.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * - * @param {Hubot.Robot} robot the robot, which will provide an HTTP - * @param {{url: string, formData: Record}} uploadData - * @param {(err: Error | null, data: any)=>void} callback - */ -function post(robot, uploadData, callback) { - robot.http(uploadData.url).post(new FormData(uploadData.formData))((err, res, body) => { - if (err) { - callback(err, null); - return; - } - - data = JSON.parse(body); - callback(null, data); - }); -} - -module.exports = { - post, -}; diff --git a/src/service/GrafanaService.js b/src/service/GrafanaService.js index cc4a0c7..db6e083 100644 --- a/src/service/GrafanaService.js +++ b/src/service/GrafanaService.js @@ -267,8 +267,12 @@ class GrafanaService { try { dashboard = await this.client.get(url); } catch (err) { - this.logger.error(err, `Error while getting dashboard on URL: ${url}`); - return null; + if (err.message !== 'Dashboard not found') { + this.logger.error(err, `Error while getting dashboard on URL: ${url}`); + return null; + } + + dashboard = { message: err.message }; } this.logger.debug(dashboard); From 8af4047597269130eb9121d386a9824814670219 Mon Sep 17 00:00:00 2001 From: "Kees C. Bakker" Date: Tue, 30 Apr 2024 21:45:01 +0200 Subject: [PATCH 2/9] Formatting. --- src/adapters/implementations/RocketChatUploader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapters/implementations/RocketChatUploader.js b/src/adapters/implementations/RocketChatUploader.js index eecb7b8..0765665 100644 --- a/src/adapters/implementations/RocketChatUploader.js +++ b/src/adapters/implementations/RocketChatUploader.js @@ -57,7 +57,7 @@ class RocketChatUploader extends Uploader { const { status } = rocketchatResBodyJson; if (status === 'success') { - return { + return { 'X-Auth-Token': rocketchatResBodyJson.data.authToken, 'X-User-Id': rocketchatResBodyJson.data.userId, }; From c669d2df66c1b68e8883650c50e49b559e9a2cdb Mon Sep 17 00:00:00 2001 From: "Kees C. Bakker" Date: Wed, 1 May 2024 06:38:52 +0200 Subject: [PATCH 3/9] No need for a trim. --- src/service/GrafanaService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/GrafanaService.js b/src/service/GrafanaService.js index db6e083..9f404bc 100644 --- a/src/service/GrafanaService.js +++ b/src/service/GrafanaService.js @@ -55,7 +55,7 @@ class GrafanaService { if (!match) return null; const request = new GrafanaDashboardRequest(); - request.uid = match[1].trim(); + request.uid = match[1]; // Parse out a specific panel if (/\:/.test(request.uid)) { From 6b8c631b1cabf4304b3ef8a144ab4b00e9265c38 Mon Sep 17 00:00:00 2001 From: "Kees C. Bakker" Date: Wed, 1 May 2024 06:39:48 +0200 Subject: [PATCH 4/9] Introducing `sendDashboardChartFromString`, which will give others the ability to send dashboards, just like they would as if they had used a hubot command. --- src/Bot.js | 43 +++++++++++++++++++++++++++++++++++++++++++ src/grafana.js | 48 ++++++++++-------------------------------------- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/src/Bot.js b/src/Bot.js index 18386b0..6857b28 100644 --- a/src/Bot.js +++ b/src/Bot.js @@ -46,6 +46,49 @@ class Bot { return new GrafanaService(client); } + /** + * Sends dashboard charts based on a request string. + * + * @param {Hubot.Response} context - The context object. + * @param {string} requestString - The request string. This string may contain all the parameters to fetch a dashboard (should not contain the `@hubot graf db` part). + * @param {number} maxReturnDashboards - The maximum number of dashboards to return. + * @returns {Promise} - A promise that resolves when the charts are sent. + */ + async sendDashboardChartFromString(context, requestString, maxReturnDashboards = null) { + const service = this.createService(context); + if (service == null) return; + + const req = service.parseToGrafanaDashboardRequest(requestString); + const dashboard = await service.getDashboard(req.uid); + + // Check dashboard information + if (!dashboard) { + return this.sendError('An error ocurred. Check your logs for more details.', context); + } + + if (dashboard.message) { + return this.sendError(dashboard.message, context); + } + + // Defaults + const data = dashboard.dashboard; + + // Handle empty dashboard + if (data.rows == null) { + return this.sendError('Dashboard empty.', context); + } + + maxReturnDashboards = maxReturnDashboards || parseInt(process.env.HUBOT_GRAFANA_MAX_RETURNED_DASHBOARDS, 10) || 25; + const charts = await service.getDashboardCharts(req, dashboard, maxReturnDashboards); + if (charts == null || charts.length === 0) { + return this.sendError('Could not locate desired panel.', context); + } + + for (let chart of charts) { + await this.sendDashboardChart(context, chart); + } + } + /** * Sends a dashboard chart. * diff --git a/src/grafana.js b/src/grafana.js index 49862a7..013f333 100644 --- a/src/grafana.js +++ b/src/grafana.js @@ -73,59 +73,31 @@ module.exports = (robot) => { }); // Get a specific dashboard with options - robot.respond(/(?:grafana|graph|graf) (?:dash|dashboard|db) ([A-Za-z0-9\-\:_]+)(.*)?/i, async (msg) => { - const service = bot.createService(msg); - if (!service) return; - - let str = msg.match[1].trim(); - if (msg.match[2]) { - str += ' ' + msg.match[2].trim(); - } + robot.respond(/(?:grafana|graph|graf) (?:dash|dashboard|db) ([A-Za-z0-9\-\:_]+)(.*)?/i, async (context) => { - const req = service.parseToGrafanaDashboardRequest(str); - const dashboard = await service.getDashboard(req.uid); - - // Check dashboard information - if (!dashboard) { - return bot.sendError('An error ocurred. Check your logs for more details.', msg); - } - if (dashboard.message) { - return bot.sendError(dashboard.message, msg); + let str = context.match[1]; + if (context.match[2]) { + str += ' ' + context.match[2]; } - // Defaults - const data = dashboard.dashboard; - - // Handle empty dashboard - if (data.rows == null) { - return bot.sendError('Dashboard empty.', msg); - } - - const dashboards = await service.getDashboardCharts(req, dashboard, maxReturnDashboards); - if (dashboards == null || dashboards.length === 0) { - return bot.sendError('Could not locate desired panel.', msg); - } - - for (let d of dashboards) { - await bot.sendDashboardChart(msg, d); - } + await bot.sendDashboardChartFromString(context, str, maxReturnDashboards); }); // Get a list of available dashboards - robot.respond(/(?:grafana|graph|graf) list\s?(.+)?/i, async (msg) => { - const service = bot.createService(msg); + robot.respond(/(?:grafana|graph|graf) list\s?(.+)?/i, async (context) => { + const service = bot.createService(context); if (!service) return; let title = 'Available dashboards:\n'; let tag = null; - if (msg.match[1]) { - tag = msg.match[1].trim(); + if (context.match[1]) { + tag = context.match[1].trim(); title = `Dashboards tagged \`${tag}\`:\n`; } const dashboards = await service.search(null, tag); if (dashboards == null) return; - sendDashboardList(dashboards, title, msg); + sendDashboardList(dashboards, title, context); }); // Search dashboards From cce94d2e543da44f8b8e61a4199c1c96a571a1fa Mon Sep 17 00:00:00 2001 From: "Kees C. Bakker" Date: Wed, 1 May 2024 06:45:36 +0200 Subject: [PATCH 5/9] Harmonize the send error. --- src/Bot.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Bot.js b/src/Bot.js index 6857b28..71b1bac 100644 --- a/src/Bot.js +++ b/src/Bot.js @@ -111,8 +111,7 @@ class Bot { try { file = await service.client.download(dashboard.imageUrl); } catch (err) { - this.sendError(err, context); - return; + return this.sendError(err, context); } this.logger.debug(`Uploading file: ${file.body.length} bytes, content-type[${file.contentType}]`); From ecbfca95234dc9c8b6041cac5c1febcd4196f23f Mon Sep 17 00:00:00 2001 From: "Kees C. Bakker" Date: Wed, 1 May 2024 06:49:37 +0200 Subject: [PATCH 6/9] Please test coverage. --- .vscode/settings.json | 1 + src/service/GrafanaService.js | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index be4ddea..2237734 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "sparkline", "stephenyeargin", "templating", + "timespan", "xaxis", "yaxes", "yaxis", diff --git a/src/service/GrafanaService.js b/src/service/GrafanaService.js index 9f404bc..7bda805 100644 --- a/src/service/GrafanaService.js +++ b/src/service/GrafanaService.js @@ -408,9 +408,7 @@ class GrafanaService { * @returns {string} - The formatted title. */ function formatTitleWithTemplate(title, templateMap) { - if (!title) { - title = ''; - } + title = title || ''; return title.replace(/\$\w+/g, (match) => { if (templateMap[match]) { return templateMap[match]; From 95554db0c5588ec11437b636132dedb214751d76 Mon Sep 17 00:00:00 2001 From: "Kees C. Bakker" Date: Wed, 1 May 2024 04:58:28 +0000 Subject: [PATCH 7/9] Try to fix mocha test runner --- .devcontainer/devcontainer.json | 3 ++- .vscode/settings.json | 2 ++ package-lock.json | 4 ++-- test/mocha.opts | 6 ++++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 995d20b..bf548b4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,7 +24,8 @@ "extensions": [ "esbenp.prettier-vscode", "GitHub.copilot", - "streetsidesoftware.code-spell-checker" + "streetsidesoftware.code-spell-checker", + "hbenl.vscode-mocha-test-adapter" ] } } diff --git a/.vscode/settings.json b/.vscode/settings.json index 2237734..f9356b5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,9 @@ "cSpell.words": [ "autofitpanels", "datasource", + "esbenp", "gnet", + "hbenl", "linewidth", "pointradius", "sparkline", diff --git a/package-lock.json b/package-lock.json index 2a739f8..3dca5e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hubot-grafana", - "version": "6.0.0", + "version": "6.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hubot-grafana", - "version": "6.0.0", + "version": "6.0.1", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.565.0", diff --git a/test/mocha.opts b/test/mocha.opts index 237f419..e664d3a 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,4 +1,6 @@ ---reporter list ---require test/test_helper.js +--reporter spec --colors --full-trace +--no-experimental-fetch +--timeout 200 + From 156018ab0729d359531c42c77828449052b8de84 Mon Sep 17 00:00:00 2001 From: "Kees C. Bakker" Date: Wed, 1 May 2024 06:46:52 +0000 Subject: [PATCH 8/9] `mocha.opts` is out of support since v8 Now it works with the dev container. --- .devcontainer/Dockerfile | 2 +- .mocharc.json | 9 +++++++++ package.json | 4 ++-- test/mocha.opts | 6 ------ 4 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 .mocharc.json delete mode 100644 test/mocha.opts diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1713208..09cc3a5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,7 +1,7 @@ FROM mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye # Install the npm packages globally -RUN npm install -g npm \ +RUN npm install -g npm@10.7.0 \ && npm install -g npm-check-updates COPY ./startup.sh / diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 0000000..0437f63 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,9 @@ +{ + "reporter": "spec", + "color": true, + "full-trace": true, + "experimental-fetch": false, + "timeout": 200, + "spec": "test/**/*-test.js" + } + \ No newline at end of file diff --git a/package.json b/package.json index f1730fc..7e750b1 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ }, "main": "index.js", "scripts": { - "test": "mocha \"test/**/*.js\" --reporter spec --no-experimental-fetch --timeout 200", - "test-with-coverage": "nyc --reporter=text mocha \"test/**/*.js\" --reporter spec", + "test": "mocha", + "test-with-coverage": "nyc --reporter=text mocha", "bootstrap": "script/bootstrap", "prepare": "husky", "lint": "eslint src/ test/" diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index e664d3a..0000000 --- a/test/mocha.opts +++ /dev/null @@ -1,6 +0,0 @@ ---reporter spec ---colors ---full-trace ---no-experimental-fetch ---timeout 200 - From 23432eed05f136f132b17c1bc42cb5cffe14b366 Mon Sep 17 00:00:00 2001 From: "Kees C. Bakker" Date: Wed, 1 May 2024 07:04:24 +0000 Subject: [PATCH 9/9] Formatting. --- .mocharc.json | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.mocharc.json b/.mocharc.json index 0437f63..4b5f285 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,9 +1,8 @@ { - "reporter": "spec", - "color": true, - "full-trace": true, - "experimental-fetch": false, - "timeout": 200, - "spec": "test/**/*-test.js" - } - \ No newline at end of file + "reporter": "spec", + "color": true, + "full-trace": true, + "experimental-fetch": false, + "timeout": 200, + "spec": "test/**/*-test.js" +}