Merge branch 'master' into security/improved-security

This commit is contained in:
Julian Speckmann
2026-01-02 04:29:06 +01:00
committed by GitHub
46 changed files with 2484 additions and 1878 deletions
+22
View File
@@ -0,0 +1,22 @@
# Dependabot configuration for Uptime Kuma
# See: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
# Enable version updates for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
# Group all GitHub Actions updates into a single PR
groups:
github-actions:
patterns:
- "*"
open-pull-requests-limit: 5
commit-message:
prefix: "chore"
include: "scope"
cooldown:
default-days: 7
+25 -12
View File
@@ -12,12 +12,15 @@ on:
branches: [ master, 1.23.X ] branches: [ master, 1.23.X ]
paths-ignore: paths-ignore:
- '*.md' - '*.md'
permissions: {}
jobs: jobs:
auto-test: auto-test:
needs: [ e2e-test ] needs: [ e2e-test ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 15 timeout-minutes: 15
permissions:
contents: read
strategy: strategy:
matrix: matrix:
@@ -31,17 +34,18 @@ jobs:
steps: steps:
- run: git config --global core.autocrlf false # Mainly for Windows - run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v4 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: { persist-credentials: false }
- name: Cache/Restore node_modules - name: Cache/Restore node_modules
uses: actions/cache@v4 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
id: node-modules-cache id: node-modules-cache
with: with:
path: node_modules path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- name: Use Node.js ${{ matrix.node }} - name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v6 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- run: npm install - run: npm install
@@ -56,6 +60,8 @@ jobs:
needs: [ e2e-test ] needs: [ e2e-test ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 15 timeout-minutes: 15
permissions:
contents: read
if: ${{ github.repository == 'louislam/uptime-kuma' }} if: ${{ github.repository == 'louislam/uptime-kuma' }}
strategy: strategy:
matrix: matrix:
@@ -65,37 +71,41 @@ jobs:
steps: steps:
- run: git config --global core.autocrlf false # Mainly for Windows - run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v4 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: { persist-credentials: false }
- name: Cache/Restore node_modules - name: Cache/Restore node_modules
uses: actions/cache@v4 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
id: node-modules-cache id: node-modules-cache
with: with:
path: node_modules path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- name: Use Node.js ${{ matrix.node }} - name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v6 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- run: npm install --production - run: npm install --production
check-linters: check-linters:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- run: git config --global core.autocrlf false # Mainly for Windows - run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v4 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: { persist-credentials: false }
- name: Cache/Restore node_modules - name: Cache/Restore node_modules
uses: actions/cache@v4 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
id: node-modules-cache id: node-modules-cache
with: with:
path: node_modules path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- name: Use Node.js 20 - name: Use Node.js 20
uses: actions/setup-node@v6 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: 20 node-version: 20
- run: npm install - run: npm install
@@ -104,21 +114,24 @@ jobs:
e2e-test: e2e-test:
needs: [ check-linters ] needs: [ check-linters ]
runs-on: ARM64 runs-on: ARM64
permissions:
contents: read
env: env:
PLAYWRIGHT_VERSION: ~1.39.0 PLAYWRIGHT_VERSION: ~1.39.0
steps: steps:
- run: git config --global core.autocrlf false # Mainly for Windows - run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v4 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: { persist-credentials: false }
- name: Cache/Restore node_modules - name: Cache/Restore node_modules
uses: actions/cache@v4 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
id: node-modules-cache id: node-modules-cache
with: with:
path: node_modules path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: 22 node-version: 22
- run: npm install - run: npm install
+10 -3
View File
@@ -3,10 +3,13 @@ name: Close Incorrect Issue
on: on:
issues: issues:
types: [opened] types: [opened]
permissions: {}
jobs: jobs:
close-incorrect-issue: close-incorrect-issue:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
permissions:
issues: write
strategy: strategy:
matrix: matrix:
@@ -14,11 +17,15 @@ jobs:
node-version: [20] node-version: [20]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: { persist-credentials: false }
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: npm ci - run: npm ci
- run: node extra/close-incorrect-issue.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.issue.number }} ${{ github.event.issue.user.login }} - name: Close incorrect issue
run: node extra/close-incorrect-issue.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.issue.number }} "$ISSUE_USER_LOGIN"
env:
ISSUE_USER_LOGIN: ${{ github.event.issue.user.login }}
+5 -5
View File
@@ -25,19 +25,19 @@ jobs:
language: [ 'go', 'javascript-typescript' ] language: [ 'go', 'javascript-typescript' ]
steps: steps:
- name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@v3 with: { persist-credentials: false }
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"
+7 -2
View File
@@ -1,6 +1,11 @@
name: Merge Conflict Labeler name: Merge Conflict Labeler
on: # pull_request_target is safe here because:
# 1. Only uses a pinned trusted action (by SHA)
# 2. Has minimal permissions (contents: read, pull-requests: write)
# 3. Doesn't checkout or execute any untrusted code from PRs
# 4. Only adds/removes labels based on merge conflict status
on: # zizmor: ignore[dangerous-triggers]
push: push:
branches: branches:
- master - master
@@ -19,7 +24,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Apply label - name: Apply label
uses: eps1lon/actions-label-merge-conflict@v3 uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
with: with:
dirtyLabel: 'needs:resolve-merge-conflict' dirtyLabel: 'needs:resolve-merge-conflict'
repoToken: '${{ secrets.GITHUB_TOKEN }}' repoToken: '${{ secrets.GITHUB_TOKEN }}'
+4 -1
View File
@@ -2,13 +2,16 @@ name: prevent-file-change
on: on:
pull_request: pull_request:
permissions: {}
jobs: jobs:
check-file-changes: check-file-changes:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pull-requests: read
steps: steps:
- name: Prevent file change - name: Prevent file change
uses: xalvarez/prevent-file-change-action@v1 uses: xalvarez/prevent-file-change-action@004d9f17c2e4a7afa037cda5f38dc55a5e9c9c06 # v1.9.1
with: with:
githubToken: ${{ secrets.GITHUB_TOKEN }} githubToken: ${{ secrets.GITHUB_TOKEN }}
# Regex, /src/lang/*.json is not allowed to be changed, except for /src/lang/en.json # Regex, /src/lang/*.json is not allowed to be changed, except for /src/lang/en.json
+7 -3
View File
@@ -4,12 +4,16 @@ on:
schedule: schedule:
- cron: '0 */6 * * *' - cron: '0 */6 * * *'
#Run every 6 hours #Run every 6 hours
permissions: {}
jobs: jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
actions: write
issues: write
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with: with:
stale-issue-message: |- stale-issue-message: |-
We are clearing up our old `help`-issues and your issue has been open for 60 days with no activity. We are clearing up our old `help`-issues and your issue has been open for 60 days with no activity.
@@ -18,10 +22,10 @@ jobs:
days-before-close: 7 days-before-close: 7
days-before-pr-stale: -1 days-before-pr-stale: -1
days-before-pr-close: -1 days-before-pr-close: -1
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request' exempt-issue-labels: 'News,discussion,bug,doc,feature-request'
exempt-issue-assignees: 'louislam' exempt-issue-assignees: 'louislam'
operations-per-run: 200 operations-per-run: 200
- uses: actions/stale@v9 - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with: with:
stale-issue-message: |- stale-issue-message: |-
This issue was marked as `cannot-reproduce` by a maintainer. This issue was marked as `cannot-reproduce` by a maintainer.
+12 -8
View File
@@ -8,20 +8,21 @@ on:
- master - master
- 1.23.X - 1.23.X
workflow_dispatch: workflow_dispatch:
permissions: {}
permissions:
contents: read
pull-requests: write # enable write permissions for pull request comments
jobs: jobs:
json-yaml-validate: json-yaml-validate:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # enable write permissions for pull request comments
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: { persist-credentials: false }
- name: json-yaml-validate - name: json-yaml-validate
id: json-yaml-validate id: json-yaml-validate
uses: GrantBirki/json-yaml-validate@v2.4.0 uses: GrantBirki/json-yaml-validate@9bbaa8474e3af4e91f25eda8ac194fdc30564d96 # v4.0.0
with: with:
comment: "true" # enable comment mode comment: "true" # enable comment mode
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions
@@ -29,10 +30,13 @@ jobs:
# General validations # General validations
validate: validate:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: { persist-credentials: false }
- name: Use Node.js 20 - name: Use Node.js 20
uses: actions/setup-node@v6 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: 20 node-version: 20
@@ -0,0 +1,30 @@
exports.up = async function (knex) {
const notifications = await knex("notification").select("id", "config");
const lineNotifyIDs = [];
for (const { id, config } of notifications) {
try {
const parsedConfig = JSON.parse(config || "{}");
const type = typeof parsedConfig.type === "string" ? parsedConfig.type.toLowerCase() : "";
if (type === "linenotify" || type === "line-notify") {
lineNotifyIDs.push(id);
}
} catch (error) {
// Ignore invalid JSON blobs here; they are handled elsewhere in the app.
}
}
if (lineNotifyIDs.length === 0) {
return;
}
await knex.transaction(async (trx) => {
await trx("monitor_notification").whereIn("notification_id", lineNotifyIDs).del();
await trx("notification").whereIn("id", lineNotifyIDs).del();
});
};
exports.down = async function () {
// Removal of LINE Notify configs is not reversible.
};
+694 -782
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -116,14 +116,14 @@
"mitt": "~3.0.1", "mitt": "~3.0.1",
"mongodb": "~4.17.1", "mongodb": "~4.17.1",
"mqtt": "~4.3.7", "mqtt": "~4.3.7",
"mssql": "~11.0.0", "mssql": "~12.0.0",
"mysql2": "~3.11.3", "mysql2": "~3.11.3",
"nanoid": "~3.3.4", "nanoid": "~3.3.4",
"net-snmp": "^3.11.2", "net-snmp": "^3.11.2",
"node-cloudflared-tunnel": "~1.0.9", "node-cloudflared-tunnel": "~1.0.9",
"node-radius-utils": "~1.2.0",
"node-fetch-cache": "^5.1.0", "node-fetch-cache": "^5.1.0",
"nodemailer": "~7.0.12", "nodemailer": "~7.0.12",
"node-radius-utils": "~1.2.0",
"nostr-tools": "^2.10.4", "nostr-tools": "^2.10.4",
"notp": "~2.0.3", "notp": "~2.0.3",
"openid-client": "^5.4.2", "openid-client": "^5.4.2",
@@ -161,6 +161,7 @@
"@playwright/test": "~1.39.0", "@playwright/test": "~1.39.0",
"@popperjs/core": "~2.10.2", "@popperjs/core": "~2.10.2",
"@testcontainers/hivemq": "^10.13.1", "@testcontainers/hivemq": "^10.13.1",
"@testcontainers/mssqlserver": "^10.28.0",
"@testcontainers/postgresql": "^11.9.0", "@testcontainers/postgresql": "^11.9.0",
"@testcontainers/rabbitmq": "^10.13.2", "@testcontainers/rabbitmq": "^10.13.2",
"@types/bootstrap": "~5.1.9", "@types/bootstrap": "~5.1.9",
+2 -2
View File
@@ -172,9 +172,9 @@ class DomainExpiry extends BeanModel {
} }
/** /**
* @returns {(Date|null)} Expiry date from RDAP * @returns {Promise<(Date|null)>} Expiry date from RDAP
*/ */
getExpiryDate() { async getExpiryDate() {
return getRdapDomainExpiryDate(this.domain); return getRdapDomainExpiryDate(this.domain);
} }
+1 -9
View File
@@ -8,7 +8,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MI
PING_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT, PING_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT,
PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT
} = require("../../src/util"); } = require("../../src/util");
const { ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, mysqlQuery, setSetting, httpNtlm, radius, const { ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mysqlQuery, setSetting, httpNtlm, radius,
kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal, checkCertificateHostname kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal, checkCertificateHostname
} = require("../util-server"); } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
@@ -782,14 +782,6 @@ class Monitor extends BeanModel {
} else { } else {
throw Error("Container State is " + res.data.State.Status); throw Error("Container State is " + res.data.State.Status);
} }
} else if (this.type === "sqlserver") {
let startTime = dayjs().valueOf();
await mssqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "mysql") { } else if (this.type === "mysql") {
let startTime = dayjs().valueOf(); let startTime = dayjs().valueOf();
+118
View File
@@ -0,0 +1,118 @@
const { MonitorType } = require("./monitor-type");
const { log, UP } = require("../../src/util");
const dayjs = require("dayjs");
const mssql = require("mssql");
const { ConditionVariable } = require("../monitor-conditions/variables");
const { defaultStringOperators } = require("../monitor-conditions/operators");
const {
ConditionExpressionGroup,
} = require("../monitor-conditions/expression");
const { evaluateExpressionGroup } = require("../monitor-conditions/evaluator");
class MssqlMonitorType extends MonitorType {
name = "sqlserver";
supportsConditions = true;
conditionVariables = [
new ConditionVariable("result", defaultStringOperators),
];
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
let startTime = dayjs().valueOf();
let query = monitor.databaseQuery;
// No query provided by user, use SELECT 1
if (!query || (typeof query === "string" && query.trim() === "")) {
query = "SELECT 1";
}
let result;
try {
result = await this.mssqlQuery(
monitor.databaseConnectionString,
query
);
} catch (error) {
log.error("sqlserver", "Database query failed:", error.message);
throw new Error(
`Database connection/query failed: ${error.message}`
);
} finally {
heartbeat.ping = dayjs().valueOf() - startTime;
}
const conditions = ConditionExpressionGroup.fromMonitor(monitor);
const handleConditions = (data) =>
conditions ? evaluateExpressionGroup(conditions, data) : true;
// Since result is now a single value, pass it directly to conditions
const conditionsResult = handleConditions({ result: String(result) });
if (!conditionsResult) {
throw new Error(
`Query result did not meet the specified conditions (${result})`
);
}
heartbeat.msg = "";
heartbeat.status = UP;
}
/**
* Run a query on MSSQL server
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<any>} Single value from the first column of the first row
*/
async mssqlQuery(connectionString, query) {
let pool;
try {
pool = new mssql.ConnectionPool(connectionString);
await pool.connect();
const result = await pool.request().query(query);
// Check if we have results
if (!result.recordset || result.recordset.length === 0) {
throw new Error("Query returned no results");
}
// Check if we have multiple rows
if (result.recordset.length > 1) {
throw new Error(
"Multiple values were found, expected only one value"
);
}
const firstRow = result.recordset[0];
const columnNames = Object.keys(firstRow);
// Check if we have multiple columns
if (columnNames.length > 1) {
throw new Error(
"Multiple columns were found, expected only one value"
);
}
// Return the single value from the first (and only) column
return firstRow[columnNames[0]];
} catch (err) {
log.debug(
"sqlserver",
"Error caught in the query execution.",
err.message
);
throw err;
} finally {
if (pool) {
await pool.close();
}
}
}
}
module.exports = {
MssqlMonitorType,
};
+47 -13
View File
@@ -1,6 +1,26 @@
const { MonitorType } = require("./monitor-type"); const { MonitorType } = require("./monitor-type");
const WebSocket = require("ws"); const WebSocket = require("ws");
const { UP } = require("../../src/util"); const { UP } = require("../../src/util");
const { checkStatusCode } = require("../util-server");
// Define closing error codes https://www.iana.org/assignments/websocket/websocket.xml#close-code-number
const WS_ERR_CODE = {
1002: "Protocol error",
1003: "Unsupported Data",
1005: "No Status Received",
1006: "Abnormal Closure",
1007: "Invalid frame payload data",
1008: "Policy Violation",
1009: "Message Too Big",
1010: "Mandatory Extension Missing",
1011: "Internal Error",
1012: "Service Restart",
1013: "Try Again Later",
1014: "Bad Gateway",
1015: "TLS Handshake Failed",
3000: "Unauthorized",
3003: "Forbidden",
3008: "Timeout",
};
class WebSocketMonitorType extends MonitorType { class WebSocketMonitorType extends MonitorType {
name = "websocket-upgrade"; name = "websocket-upgrade";
@@ -11,24 +31,36 @@ class WebSocketMonitorType extends MonitorType {
async check(monitor, heartbeat, _server) { async check(monitor, heartbeat, _server) {
const [ message, code ] = await this.attemptUpgrade(monitor); const [ message, code ] = await this.attemptUpgrade(monitor);
if (code === 1000) { if (typeof code !== "undefined") {
heartbeat.status = UP; // If returned status code matches user controlled accepted status code(default 1000), return success
heartbeat.msg = message; if (checkStatusCode(code, JSON.parse(monitor.accepted_statuscodes_json))) {
} else { heartbeat.status = UP;
throw new Error(message); heartbeat.msg = message;
return; // success at this point
}
// Throw an error using friendly name if defined, fallback to generic msg
throw new Error(WS_ERR_CODE[code] || `Unexpected status code: ${code}`);
} }
// If no close code, then an error has occurred, display to user
if (typeof message !== "undefined") {
throw new Error(`${message}`);
}
// Throw generic error if nothing is defined, should never happen
throw new Error("Unknown Websocket Error");
} }
/** /**
* Uses the builtin Websocket API to establish a connection to target server * Uses the ws Node.js library to establish a connection to target server
* @param {object} monitor The monitor object for input parameters. * @param {object} monitor The monitor object for input parameters.
* @returns {[ string, int ]} Array containing a status message and response code * @returns {[ string, int ]} Array containing a status message and response code
*/ */
async attemptUpgrade(monitor) { async attemptUpgrade(monitor) {
return new Promise((resolve) => { return new Promise((resolve) => {
let ws; const timeoutMs = (monitor.timeout ?? 20) * 1000;
//If user selected a subprotocol, sets Sec-WebSocket-Protocol header. Subprotocol Identifier column: https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name // If user inputs subprotocol(s), convert to array, set Sec-WebSocket-Protocol header, timeout in ms. Subprotocol Identifier column: https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name
ws = monitor.wsSubprotocol === "" ? new WebSocket(monitor.url) : new WebSocket(monitor.url, monitor.wsSubprotocol); const subprotocol = monitor.wsSubprotocol ? monitor.wsSubprotocol.replace(/\s/g, "").split(",") : undefined;
const ws = new WebSocket(monitor.url, subprotocol, { handshakeTimeout: timeoutMs });
ws.addEventListener("open", (event) => { ws.addEventListener("open", (event) => {
// Immediately close the connection // Immediately close the connection
@@ -36,17 +68,19 @@ class WebSocketMonitorType extends MonitorType {
}); });
ws.onerror = (error) => { ws.onerror = (error) => {
// Give user the choice to ignore Sec-WebSocket-Accept header // Give user the choice to ignore Sec-WebSocket-Accept header for non compliant servers
// Header in HTTP 101 Switching Protocols response from server, technically already upgraded to WS
if (monitor.wsIgnoreSecWebsocketAcceptHeader && error.message === "Invalid Sec-WebSocket-Accept header") { if (monitor.wsIgnoreSecWebsocketAcceptHeader && error.message === "Invalid Sec-WebSocket-Accept header") {
resolve([ "101 - OK", 1000 ]); resolve([ "1000 - OK", 1000 ]);
return;
} }
// Upgrade failed, return message to user // Upgrade failed, return message to user
resolve([ error.message, error.code ]); resolve([ error.message, error.code ]);
}; };
ws.onclose = (event) => { ws.onclose = (event) => {
// Upgrade success, connection closed successfully // Return the close code, if connection didn't close cleanly, return the reason if present
resolve([ "101 - OK", event.code ]); resolve([ event.wasClean ? event.code.toString() + " - OK" : event.reason, event.code ]);
}; };
}); });
} }
@@ -1,53 +0,0 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const qs = require("qs");
const { DOWN, UP } = require("../../src/util");
class LineNotify extends NotificationProvider {
name = "LineNotify";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://notify-api.line.me/api/notify";
try {
let config = {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Bearer " + notification.lineNotifyAccessToken
}
};
config = this.getAxiosConfigWithProxy(config);
if (heartbeatJSON == null) {
let testMessage = {
"message": msg,
};
await axios.post(url, qs.stringify(testMessage), config);
} else if (heartbeatJSON["status"] === DOWN) {
let downMessage = {
"message": "\n[🔴 Down]\n" +
"Name: " + monitorJSON["name"] + " \n" +
heartbeatJSON["msg"] + "\n" +
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
};
await axios.post(url, qs.stringify(downMessage), config);
} else if (heartbeatJSON["status"] === UP) {
let upMessage = {
"message": "\n[✅ Up]\n" +
"Name: " + monitorJSON["name"] + " \n" +
heartbeatJSON["msg"] + "\n" +
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
};
await axios.post(url, qs.stringify(upMessage), config);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = LineNotify;
+48
View File
@@ -0,0 +1,48 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Resend extends NotificationProvider {
name = "Resend";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let config = {
headers: {
Authorization: `Bearer ${notification.resendApiKey}`,
"Content-Type": "application/json",
},
};
config = this.getAxiosConfigWithProxy(config);
const email = notification.resendFromEmail.trim();
const fromName = notification.resendFromName?.trim() || "Uptime Kuma";
let data = {
from: `${fromName} <${email}>`,
to: notification.resendToEmail,
subject: notification.resendSubject || "Notification from Your Uptime Kuma",
// supplied text directly instead of html
text: msg,
};
let result = await axios.post(
"https://api.resend.com/emails",
data,
config
);
if (result.status === 200) {
return okMsg;
} else {
throw new Error(`Unexpected status code: ${result.status}`);
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Resend;
+2 -2
View File
@@ -25,7 +25,6 @@ const HeiiOnCall = require("./notification-providers/heii-oncall");
const Keep = require("./notification-providers/keep"); const Keep = require("./notification-providers/keep");
const Kook = require("./notification-providers/kook"); const Kook = require("./notification-providers/kook");
const Line = require("./notification-providers/line"); const Line = require("./notification-providers/line");
const LineNotify = require("./notification-providers/linenotify");
const LunaSea = require("./notification-providers/lunasea"); const LunaSea = require("./notification-providers/lunasea");
const Matrix = require("./notification-providers/matrix"); const Matrix = require("./notification-providers/matrix");
const Mattermost = require("./notification-providers/mattermost"); const Mattermost = require("./notification-providers/mattermost");
@@ -78,6 +77,7 @@ const Onesender = require("./notification-providers/onesender");
const Wpush = require("./notification-providers/wpush"); const Wpush = require("./notification-providers/wpush");
const SendGrid = require("./notification-providers/send-grid"); const SendGrid = require("./notification-providers/send-grid");
const Brevo = require("./notification-providers/brevo"); const Brevo = require("./notification-providers/brevo");
const Resend = require("./notification-providers/resend");
const YZJ = require("./notification-providers/yzj"); const YZJ = require("./notification-providers/yzj");
const SMSPlanet = require("./notification-providers/sms-planet"); const SMSPlanet = require("./notification-providers/sms-planet");
const SpugPush = require("./notification-providers/spugpush"); const SpugPush = require("./notification-providers/spugpush");
@@ -124,7 +124,6 @@ class Notification {
new Keep(), new Keep(),
new Kook(), new Kook(),
new Line(), new Line(),
new LineNotify(),
new LunaSea(), new LunaSea(),
new Matrix(), new Matrix(),
new Mattermost(), new Mattermost(),
@@ -176,6 +175,7 @@ class Notification {
new Cellsynt(), new Cellsynt(),
new Wpush(), new Wpush(),
new Brevo(), new Brevo(),
new Resend(),
new YZJ(), new YZJ(),
new SMSPlanet(), new SMSPlanet(),
new SpugPush(), new SpugPush(),
+2
View File
@@ -124,6 +124,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["port"] = new TCPMonitorType(); UptimeKumaServer.monitorTypeList["port"] = new TCPMonitorType();
UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType(); UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType();
UptimeKumaServer.monitorTypeList["redis"] = new RedisMonitorType(); UptimeKumaServer.monitorTypeList["redis"] = new RedisMonitorType();
UptimeKumaServer.monitorTypeList["sqlserver"] = new MssqlMonitorType();
// Allow all CORS origins (polling) in development // Allow all CORS origins (polling) in development
let cors = undefined; let cors = undefined;
@@ -572,5 +573,6 @@ const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
const { TCPMonitorType } = require("./monitor-types/tcp.js"); const { TCPMonitorType } = require("./monitor-types/tcp.js");
const { ManualMonitorType } = require("./monitor-types/manual"); const { ManualMonitorType } = require("./monitor-types/manual");
const { RedisMonitorType } = require("./monitor-types/redis"); const { RedisMonitorType } = require("./monitor-types/redis");
const { MssqlMonitorType } = require("./monitor-types/mssql");
const Monitor = require("./model/monitor"); const Monitor = require("./model/monitor");
-26
View File
@@ -10,7 +10,6 @@ const { Resolver } = require("dns");
const iconv = require("iconv-lite"); const iconv = require("iconv-lite");
const chardet = require("chardet"); const chardet = require("chardet");
const chroma = require("chroma-js"); const chroma = require("chroma-js");
const mssql = require("mssql");
const mysql = require("mysql2"); const mysql = require("mysql2");
const { NtlmClient } = require("./modules/axios-ntlm/lib/ntlmClient.js"); const { NtlmClient } = require("./modules/axios-ntlm/lib/ntlmClient.js");
const { Settings } = require("./settings"); const { Settings } = require("./settings");
@@ -322,31 +321,6 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
}); });
}; };
/**
* Run a query on SQL Server
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[] | object[] | object)>} Response from
* server
*/
exports.mssqlQuery = async function (connectionString, query) {
let pool;
try {
pool = new mssql.ConnectionPool(connectionString);
await pool.connect();
if (!query) {
query = "SELECT 1";
}
await pool.request().query(query);
pool.close();
} catch (e) {
if (pool) {
pool.close();
}
throw e;
}
};
/** /**
* Run a query on MySQL/MariaDB * Run a query on MySQL/MariaDB
* @param {string} connectionString The database connection string * @param {string} connectionString The database connection string
+1 -1
View File
@@ -1,6 +1,6 @@
html[lang='fa'] { html[lang='fa'] {
#app { #app {
font-family: 'IRANSans', 'Iranian Sans','B Nazanin', 'Tahoma', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji; font-family: 'Vazirmatn', 'IRANSans', 'Iranian Sans','B Nazanin', 'Tahoma', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
} }
} }
+1 -1
View File
@@ -129,7 +129,6 @@ export default {
"Keep": "Keep", "Keep": "Keep",
"Kook": "Kook", "Kook": "Kook",
"line": "LINE Messenger", "line": "LINE Messenger",
"LineNotify": "LINE Notify",
"lunasea": "LunaSea", "lunasea": "LunaSea",
"matrix": "Matrix", "matrix": "Matrix",
"mattermost": "Mattermost", "mattermost": "Mattermost",
@@ -173,6 +172,7 @@ export default {
"Cellsynt": "Cellsynt", "Cellsynt": "Cellsynt",
"SendGrid": "SendGrid", "SendGrid": "SendGrid",
"Brevo": "Brevo", "Brevo": "Brevo",
"Resend": "Resend",
"notifery": "Notifery", "notifery": "Notifery",
"Webpush": "Webpush", "Webpush": "Webpush",
}; };
@@ -1,9 +0,0 @@
<template>
<div class="mb-3">
<label for="line-notify-access-token" class="form-label">{{ $t("Access Token") }}</label>
<input id="line-notify-access-token" v-model="$parent.notification.lineNotifyAccessToken" type="text" class="form-control" :required="true">
</div>
<i18n-t tag="div" keypath="wayToGetLineNotifyToken" class="form-text" style="margin-top: 8px;">
<a href="https://notify-bot.line.me/" target="_blank">https://notify-bot.line.me/</a>
</i18n-t>
</template>
+48
View File
@@ -0,0 +1,48 @@
<template>
<div class="mb-3">
<label for="resend-api-key" class="form-label">{{ $t("resendApiKey") }}</label>
<HiddenInput id="resend-api-key" v-model="$parent.notification.resendApiKey" :required="true" autocomplete="new-password"></HiddenInput>
<i18n-t tag="div" keypath="resendApiHelp" class="form-text">
<a href="https://resend.com/api-keys" target="_blank">https://resend.com/api-keys</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="resend-from-email" class="form-label">{{ $t("resendFromEmail") }}</label>
<input id="resend-from-email" v-model="$parent.notification.resendFromEmail" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="resend-from-name" class="form-label">{{ $t("resendFromName") }}</label>
<input id="resend-from-name" v-model="$parent.notification.resendFromName" type="text" class="form-control">
<div class="form-text">{{ $t("resendLeaveBlankForDefaultName") }}</div>
</div>
<div class="mb-3">
<label for="resend-to-email" class="form-label">{{ $t("resendToEmail") }}</label>
<input id="resend-to-email" v-model="$parent.notification.resendToEmail" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="resend-subject" class="form-label">{{ $t("resendSubject") }}</label>
<input id="resend-subject" v-model="$parent.notification.resendSubject" type="text" class="form-control">
<small class="form-text text-muted">{{ $t("resendLeaveBlankForDefaultSubject") }}</small>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://resend.com/docs/dashboard/emails/introduction" target="_blank">https://resend.com/docs/dashboard/emails/introduction</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
mounted() {
if (typeof this.$parent.notification.resendSubject === "undefined") {
this.$parent.notification.resendSubject = "Notification from Your Uptime Kuma";
}
if (typeof this.$parent.notification.resendFromName === "undefined") {
this.$parent.notification.resendFromName = "Uptime Kuma";
}
},
};
</script>
+2 -2
View File
@@ -24,7 +24,6 @@ import HeiiOnCall from "./HeiiOnCall.vue";
import Keep from "./Keep.vue"; import Keep from "./Keep.vue";
import Kook from "./Kook.vue"; import Kook from "./Kook.vue";
import Line from "./Line.vue"; import Line from "./Line.vue";
import LineNotify from "./LineNotify.vue";
import LunaSea from "./LunaSea.vue"; import LunaSea from "./LunaSea.vue";
import Matrix from "./Matrix.vue"; import Matrix from "./Matrix.vue";
import Mattermost from "./Mattermost.vue"; import Mattermost from "./Mattermost.vue";
@@ -81,6 +80,7 @@ import YZJ from "./YZJ.vue";
import SMSPlanet from "./SMSPlanet.vue"; import SMSPlanet from "./SMSPlanet.vue";
import SMSIR from "./SMSIR.vue"; import SMSIR from "./SMSIR.vue";
import Webpush from "./Webpush.vue"; import Webpush from "./Webpush.vue";
import Resend from "./Resend.vue";
/** /**
* Manage all notification form. * Manage all notification form.
@@ -112,7 +112,6 @@ const NotificationFormList = {
"Keep": Keep, "Keep": Keep,
"Kook": Kook, "Kook": Kook,
"line": Line, "line": Line,
"LineNotify": LineNotify,
"lunasea": LunaSea, "lunasea": LunaSea,
"matrix": Matrix, "matrix": Matrix,
"mattermost": Mattermost, "mattermost": Mattermost,
@@ -167,6 +166,7 @@ const NotificationFormList = {
"WPush": WPush, "WPush": WPush,
"SendGrid": SendGrid, "SendGrid": SendGrid,
"Brevo": Brevo, "Brevo": Brevo,
"Resend": Resend,
"YZJ": YZJ, "YZJ": YZJ,
"SMSPlanet": SMSPlanet, "SMSPlanet": SMSPlanet,
"Webpush": Webpush, "Webpush": Webpush,
+12 -34
View File
@@ -90,39 +90,9 @@
"upsideDownModeDescription": "Flip the status upside down. If the service is reachable, it is DOWN.", "upsideDownModeDescription": "Flip the status upside down. If the service is reachable, it is DOWN.",
"ignoreSecWebsocketAcceptHeaderDescription": "Allows the server to not reply with Sec-WebSocket-Accept header, if the websocket upgrade succeeds.", "ignoreSecWebsocketAcceptHeaderDescription": "Allows the server to not reply with Sec-WebSocket-Accept header, if the websocket upgrade succeeds.",
"Ignore Sec-WebSocket-Accept header": "Ignore {0} header", "Ignore Sec-WebSocket-Accept header": "Ignore {0} header",
"wsSubprotocolDescription": "For more information on subprotocols, please consult the {documentation}", "wsSubprotocolDescription": "Enter a comma delimited list of subprotocols. For more information on subprotocols, please consult the {documentation}",
"WebSocket Application Messaging Protocol": "WAMP (The WebSocket Application Messaging Protocol)", "wsCodeDescription": "For more information on status codes, please consult {rfc6455}",
"Session Initiation Protocol": "WebSocket Transport for SIP (Session Initiation Protocol)", "Subprotocol(s)": "Subprotocol(s)",
"Subprotocol": "Subprotocol",
"Network API for Notification Channel": "OMA RESTful Network API for Notification Channel",
"Web Process Control Protocol": "Web Process Control Protocol (WPCP)",
"Advanced Message Queuing Protocol": "Advanced Message Queuing Protocol (AMQP) 1.0+",
"jsflow": "jsFlow pubsub/queue Protocol",
"Reverse Web Process Control": "Reverse Web Process Control Protocol (RWPCP)",
"Extensible Messaging and Presence Protocol": "WebSocket Transport for the Extensible Messaging and Presence Protocol (XMPP)",
"Smart Home IP": "SHIP - Smart Home IP",
"Miele Cloud Connect Protocol": "Miele Cloud Connect Protocol",
"Push Channel Protocol": "Push Channel Protocol",
"Message Session Relay Protocol": "WebSocket Transport for MSRP (Message Session Relay Protocol)",
"Binary Floor Control Protocol": "WebSocket Transport for BFCP (Binary Floor Control Protocol)",
"Softvelum Low Delay Protocol": "Softvelum Low Delay Protocol",
"OPC UA Connection Protocol": "OPC UA Connection Protocol",
"OPC UA JSON Encoding": "OPC UA JSON Encoding",
"Swindon Web Server Protocol": "Swindon Web Server Protocol (JSON encoding)",
"Broadband Forum User Services Platform": "USP (Broadband Forum User Services Platform)",
"Constrained Application Protocol": "Constrained Application Protocol (CoAP)",
"Softvelum WebSocket signaling protocol": "Softvelum WebSocket Signaling Protocol",
"Cobra Real Time Messaging Protocol": "Cobra Real Time Messaging Protocol",
"Declarative Resource Protocol": "Declarative Resource Protocol",
"BACnet Secure Connect Hub Connection": "BACnet Secure Connect Hub Connection",
"BACnet Secure Connect Direct Connection": "BACnet Secure Connect Direct Connection",
"WebSocket Transport for JMAP": "WebSocket Transport for JMAP (JSON Meta Application Protocol)",
"ITU-T T.140 Real-Time Text": "ITU-T T.140 Real-Time Text",
"Done.best IoT Protocol": "Done.best IoT Protocol",
"Collection Update": "The Collection Update Websocket Subprotocol",
"Text IRC Protocol": "Text IRC Protocol",
"Binary IRC Protocol": "Binary IRC Protocol",
"Penguin Statistics Live Protocol v3": "Penguin Statistics Live Protocol v3 (Protobuf encoding)",
"maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.", "maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
"Upside Down Mode": "Upside Down Mode", "Upside Down Mode": "Upside Down Mode",
"Max. Redirects": "Max. Redirects", "Max. Redirects": "Max. Redirects",
@@ -1183,6 +1153,13 @@
"brevoSeparateMultipleEmails": "Separate multiple email addresses with commas", "brevoSeparateMultipleEmails": "Separate multiple email addresses with commas",
"brevoSubject": "Subject", "brevoSubject": "Subject",
"brevoLeaveBlankForDefaultSubject": "leave blank for default subject", "brevoLeaveBlankForDefaultSubject": "leave blank for default subject",
"resendApiKey": "Resend API Key",
"resendApiHelp": "Create an api key here {0}",
"resendFromName": "From Name",
"resendFromEmail": "From Email",
"resendLeaveBlankForDefaultName": "leave blank for default name",
"resendToEmail": "To Email",
"resendSubject": "Subject",
"pingCountLabel": "Max Packets", "pingCountLabel": "Max Packets",
"pingCountDescription": "Number of packets to send before stopping", "pingCountDescription": "Number of packets to send before stopping",
"pingNumericLabel": "Numeric Output", "pingNumericLabel": "Numeric Output",
@@ -1246,5 +1223,6 @@
"labelDomainNameExpiryNotification": "Domain Name Expiry Notification", "labelDomainNameExpiryNotification": "Domain Name Expiry Notification",
"domainExpiryDescription": "Trigger notification when domain names expires in:", "domainExpiryDescription": "Trigger notification when domain names expires in:",
"minimumIntervalWarning": "Intervals below 20 seconds may result in poor performance.", "minimumIntervalWarning": "Intervals below 20 seconds may result in poor performance.",
"lowIntervalWarning": "Are you sure want to set the interval value below 20 seconds? Performance may be degraded, particularly if there are a large number of monitors." "lowIntervalWarning": "Are you sure want to set the interval value below 20 seconds? Performance may be degraded, particularly if there are a large number of monitors.",
"imageResetConfirmation": "Image reset to default"
} }
+43 -69
View File
@@ -142,73 +142,8 @@
<!-- Websocket Subprotocol Docs: https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name --> <!-- Websocket Subprotocol Docs: https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name -->
<div v-if="monitor.type === 'websocket-upgrade'" class="my-3"> <div v-if="monitor.type === 'websocket-upgrade'" class="my-3">
<label for="ws_subprotocol" class="form-label">{{ $t("Subprotocol") }}</label> <label for="ws_subprotocol" class="form-label">{{ $t("Subprotocol(s)") }}</label>
<select id="ws_subprotocol" v-model="monitor.wsSubprotocol" class="form-select"> <input id="ws_subprotocol" v-model="monitor.wsSubprotocol" type="text" class="form-control" placeholder="mielecloudconnect,soap">
<option value="" selected>{{ $t("None") }}</option>
<option value="MBWS.huawei.com">MBWS</option>
<option value="MBLWS.huawei.com">MBLWS</option>
<option value="soap">soap</option>
<option value="wamp">{{ $t("WebSocket Application Messaging Protocol") }}</option>
<option value="v10.stomp">STOMP 1.0</option>
<option value="v11.stomp">STOMP 1.1</option>
<option value="v12.stomp">STOMP 1.2</option>
<option value="ocpp1.2">OCPP 1.2</option>
<option value="ocpp1.5">OCPP 1.5</option>
<option value="ocpp1.6">OCPP 1.6</option>
<option value="ocpp2.0">OCPP 2.0</option>
<option value="ocpp2.0.1">OCPP 2.0.1</option>
<option value="ocpp2.1">OCPP 2.1</option>
<option value="rfb">RFB</option>
<option value="sip">{{ $t("Session Initiation Protocol") }}</option>
<option value="notificationchannel-netapi-rest.openmobilealliance.org">{{ $t("Network API for Notification Channel") }}</option>
<option value="wpcp">{{ $t("Web Process Control Protocol") }}</option>
<option value="amqp">{{ $t("Advanced Message Queuing Protocol") }}</option>
<option value="mqtt">MQTT</option>
<option value="jsflow">{{ $t("jsflow") }}</option>
<option value="rwpcp">{{ $t("Reverse Web Process Control") }}</option>
<option value="xmpp">{{ $t("Extensible Messaging and Presence Protocol") }}</option>
<option value="ship">{{ $t("Smart Home IP") }}</option>
<option value="mielecloudconnect">{{ $t("Miele Cloud Connect Protocol") }}</option>
<option value="v10.pcp.sap.com">{{ $t("Push Channel Protocol") }}</option>
<option value="msrp">{{ $t("Message Session Relay Protocol") }}</option>
<option value="v1.saltyrtc.org">SaltyRTC 1.0</option>
<option value="TLCP-2.0.0.lightstreamer.com">TLCP 2.0.0</option>
<option value="bfcp">{{ $t("Binary Floor Control Protocol") }}</option>
<option value="sldp.softvelum.com">{{ $t("Softvelum Low Delay Protocol") }}</option>
<option value="opcua+uacp">{{ $t("OPC UA Connection Protocol") }}</option>
<option value="opcua+uajson">{{ $t("OPC UA JSON Encoding") }}</option>
<option value="v1.swindon-lattice+json">{{ $t("Swindon Web Server Protocol") }}</option>
<option value="v1.usp">{{ $t("Broadband Forum User Services Platform") }}</option>
<option value="mles-websocket">mles-websocket</option>
<option value="coap">{{ $t("Constrained Application Protocol") }}</option>
<option value="TLCP-2.1.0.lightstreamer.com">TLCP 2.1.0</option>
<option value="sqlnet.oracle.com">sqlnet</option>
<option value="oneM2M.R2.0.json">oneM2M R2.0 JSON</option>
<option value="oneM2M.R2.0.xml">oneM2M R2.0 XML</option>
<option value="oneM2M.R2.0.cbor">oneM2M R2.0 CBOR</option>
<option value="transit">Transit</option>
<option value="2016.serverpush.dash.mpeg.org">MPEG-DASH-ServerPush-23009-6-2017</option>
<option value="2018.mmt.mpeg.org">MPEG-MMT-23008-1-2018</option>
<option value="clue">clue</option>
<option value="webrtc.softvelum.com">{{ $t("Softvelum WebSocket signaling protocol") }}</option>
<option value="cobra.v2.json">{{ $t("Cobra Real Time Messaging Protocol") }}</option>
<option value="drp">{{ $t("Declarative Resource Protocol") }}</option>
<option value="hub.bsc.bacnet.org">{{ $t("BACnet Secure Connect Hub Connection") }}</option>
<option value="dc.bsc.bacnet.org">{{ $t("BACnet Secure Connect Direct Connection") }}</option>
<option value="jmap">{{ $t("WebSocket Transport for JMAP") }}</option>
<option value="t140">{{ $t("ITU-T T.140 Real-Time Text") }}</option>
<option value="done">{{ $t("Done.best IoT Protocol") }}</option>
<option value="TLCP-2.2.0.lightstreamer.com">TLCP 2.2.0</option>
<option value="collection-update">{{ $t("Collection Update") }}</option>
<option value="TLCP-2.3.0.lightstreamer.com">TLCP 2.3.0</option>
<option value="text.ircv3.net">{{ $t("Text IRC Protocol") }}</option>
<option value="binary.ircv3.net">{{ $t("Binary IRC Protocol") }}</option>
<option value="v3.penguin-stats.live+proto">{{ $t("Penguin Statistics Live Protocol v3") }}</option>
<option value="TLCP-2.4.0.lightstreamer.com">TLCP 2.4.0</option>
<option value="TLCP-2.5.0.lightstreamer.com">TLCP 2.5.0</option>
<option value="Redfish">Redfish DSP0266</option>
<option value="bidib">webBiDiB</option>
</select>
<i18n-t tag="div" class="form-text" keypath="wsSubprotocolDescription"> <i18n-t tag="div" class="form-text" keypath="wsSubprotocolDescription">
<template #documentation> <template #documentation>
<a href="https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name" target="_blank" rel="noopener noreferrer">{{ $t('documentationOf', ['IANA']) }}</a> <a href="https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name" target="_blank" rel="noopener noreferrer">{{ $t('documentationOf', ['IANA']) }}</a>
@@ -767,8 +702,8 @@
</div> </div>
</div> </div>
<!-- Timeout: HTTP / JSON query / Keyword / Ping / RabbitMQ / SNMP only --> <!-- Timeout: HTTP / JSON query / Keyword / Ping / RabbitMQ / SNMP / Websocket Upgrade only -->
<div v-if="monitor.type === 'http' || monitor.type === 'json-query' || monitor.type === 'keyword' || monitor.type === 'ping' || monitor.type === 'rabbitmq' || monitor.type === 'snmp'" class="my-3"> <div v-if="monitor.type === 'http' || monitor.type === 'json-query' || monitor.type === 'keyword' || monitor.type === 'ping' || monitor.type === 'rabbitmq' || monitor.type === 'snmp' || monitor.type === 'websocket-upgrade'" class="my-3">
<label for="timeout" class="form-label"> <label for="timeout" class="form-label">
{{ monitor.type === 'ping' ? $t("pingGlobalTimeoutLabel") : $t("Request Timeout") }} {{ monitor.type === 'ping' ? $t("pingGlobalTimeoutLabel") : $t("Request Timeout") }}
<span v-if="monitor.type !== 'ping'">({{ $t("timeoutAfter", [monitor.timeout || clampTimeout(monitor.interval)]) }})</span> <span v-if="monitor.type !== 'ping'">({{ $t("timeoutAfter", [monitor.timeout || clampTimeout(monitor.interval)]) }})</span>
@@ -889,6 +824,36 @@
</div> </div>
</div> </div>
<!-- Websocket Upgrade only -->
<template v-if="monitor.type === 'websocket-upgrade' ">
<div class="my-3">
<label for="acceptedStatusCodes" class="form-label">{{ $t("Accepted Status Codes") }}</label>
<VueMultiselect
id="acceptedStatusCodes"
v-model="monitor.accepted_statuscodes"
:options="acceptedWebsocketCodeOptions"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
:placeholder="$t('Pick Accepted Status Codes...')"
:preselect-first="false"
:max-height="600"
:taggable="true"
></VueMultiselect>
<div class="form-text">
{{ $t("acceptedStatusCodesDescription") }}
</div>
<i18n-t tag="div" class="form-text" keypath="wsCodeDescription">
<template #rfc6455>
<a href="https://datatracker.ietf.org/doc/html/rfc6455#section-7.4" target="_blank" rel="noopener noreferrer">RFC 6455</a>
</template>
</i18n-t>
</div>
</template>
<!-- HTTP / Keyword only --> <!-- HTTP / Keyword only -->
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'grpc-keyword' "> <template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'grpc-keyword' ">
<div class="my-3"> <div class="my-3">
@@ -1398,6 +1363,7 @@ export default {
}, },
hasDomain: false, hasDomain: false,
acceptedStatusCodeOptions: [], acceptedStatusCodeOptions: [],
acceptedWebsocketCodeOptions: [],
dnsresolvetypeOptions: [], dnsresolvetypeOptions: [],
kafkaSaslMechanismOptions: [], kafkaSaslMechanismOptions: [],
ipOrHostnameRegexPattern: hostNameRegexPattern(), ipOrHostnameRegexPattern: hostNameRegexPattern(),
@@ -1770,6 +1736,7 @@ message HealthCheckResponse {
"monitor.type"(newType, oldType) { "monitor.type"(newType, oldType) {
if (oldType && this.monitor.type === "websocket-upgrade") { if (oldType && this.monitor.type === "websocket-upgrade") {
this.monitor.url = "wss://"; this.monitor.url = "wss://";
this.monitor.accepted_statuscodes = [ "1000" ];
} }
if (this.monitor.type === "push") { if (this.monitor.type === "push") {
if (! this.monitor.pushToken) { if (! this.monitor.pushToken) {
@@ -1877,6 +1844,8 @@ message HealthCheckResponse {
"500-599", "500-599",
]; ];
let acceptedWebsocketCodeOptions = [];
let dnsresolvetypeOptions = [ let dnsresolvetypeOptions = [
"A", "A",
"AAAA", "AAAA",
@@ -1902,6 +1871,11 @@ message HealthCheckResponse {
acceptedStatusCodeOptions.push(i.toString()); acceptedStatusCodeOptions.push(i.toString());
} }
for (let i = 1000; i <= 4999; i++) {
acceptedWebsocketCodeOptions.push(i.toString());
}
this.acceptedWebsocketCodeOptions = acceptedWebsocketCodeOptions;
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions; this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
this.dnsresolvetypeOptions = dnsresolvetypeOptions; this.dnsresolvetypeOptions = dnsresolvetypeOptions;
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions; this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
+69
View File
@@ -138,6 +138,9 @@
<h1 class="mb-4 title-flex"> <h1 class="mb-4 title-flex">
<!-- Logo --> <!-- Logo -->
<span class="logo-wrapper" @click="showImageCropUploadMethod"> <span class="logo-wrapper" @click="showImageCropUploadMethod">
<button v-if="editMode" type="button" class="p-0 bg-transparent border-0 small-reset-btn reset-top-left" @click.stop="resetToDefaultImage">
<font-awesome-icon icon="times" class="text-danger" />
</button>
<img :src="logoURL" alt class="logo me-2" :class="logoClass" /> <img :src="logoURL" alt class="logo me-2" :class="logoClass" />
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" /> <font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
</span> </span>
@@ -962,6 +965,20 @@ export default {
} }
}, },
/**
* Reset logo image to default (public/icon.svg)
* @returns {void}
*/
resetToDefaultImage() {
if (! this.editMode) {
return;
}
this.imgDataUrl = "/icon.svg";
this.config.icon = this.imgDataUrl;
toast.success(this.$t("imageResetConfirmation"));
},
/** /**
* Create an incident for this status page * Create an incident for this status page
* @returns {void} * @returns {void}
@@ -1181,6 +1198,58 @@ footer {
cursor: pointer; cursor: pointer;
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.9); box-shadow: 0 15px 70px rgba(0, 0, 0, 0.9);
} }
/* Reset button placed at top-left of the logo */
.reset-top-left {
position: absolute;
top: 0;
left: -15px;
z-index: 2;
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: white;
border: none;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
cursor: pointer;
padding: 0;
transition: transform $easing-in 0.18s, box-shadow $easing-in 0.18s, background-color $easing-in 0.18s;
transform-origin: center;
&:hover {
background-color: rgba(0, 0, 0, 0.06);
transform: scale(1.18);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
}
&:hover ~ .icon-upload {
transform: none !important;
}
}
.small-reset-btn {
transition: transform $easing-in 0.18s, box-shadow $easing-in 0.18s, background-color $easing-in 0.18s;
font-size: 18px;
width: 18px;
height: 18px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: transparent;
border: none;
cursor: pointer;
&:hover {
background-color: rgba(0, 0, 0, 0.04);
transform: scale(1.18);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
}
}
} }
.logo { .logo {
+15 -3
View File
@@ -4,14 +4,26 @@ Documentation: https://nodejs.org/api/test.html
Create a test file in this directory with the name `*.js`. Create a test file in this directory with the name `*.js`.
> [!TIP]
> Writing great tests is hard.
>
> You can make our live much simpler by following this guidance:
> - Use `describe()` to group related tests
> - Use `test()` for individual test cases
> - One test per scenario
> - Use descriptive test names: `function() [behavior] [condition]`
> - Don't prefix with "Test" or "Should"
## Template ## Template
```js ```js
const test = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
test("Test name", async (t) => { describe("Feature Name", () => {
assert.strictEqual(1, 1); test("function() returns expected value when condition is met", () => {
assert.strictEqual(1, 1);
});
}); });
``` ```
@@ -1,46 +1,48 @@
const test = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("../../../server/monitor-conditions/expression.js"); const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("../../../server/monitor-conditions/expression.js");
const { evaluateExpressionGroup, evaluateExpression } = require("../../../server/monitor-conditions/evaluator.js"); const { evaluateExpressionGroup, evaluateExpression } = require("../../../server/monitor-conditions/evaluator.js");
test("Test evaluateExpression", async (t) => { describe("Expression Evaluator", () => {
const expr = new ConditionExpression("record", "contains", "mx1.example.com"); test("evaluateExpression() returns true when condition matches and false otherwise", () => {
assert.strictEqual(true, evaluateExpression(expr, { record: "mx1.example.com" })); const expr = new ConditionExpression("record", "contains", "mx1.example.com");
assert.strictEqual(false, evaluateExpression(expr, { record: "mx2.example.com" })); assert.strictEqual(true, evaluateExpression(expr, { record: "mx1.example.com" }));
}); assert.strictEqual(false, evaluateExpression(expr, { record: "mx2.example.com" }));
});
test("Test evaluateExpressionGroup with logical AND", async (t) => { test("evaluateExpressionGroup() with AND logic requires all conditions to be true", () => {
const group = new ConditionExpressionGroup([ const group = new ConditionExpressionGroup([
new ConditionExpression("record", "contains", "mx1."), new ConditionExpression("record", "contains", "mx1."),
new ConditionExpression("record", "contains", "example.com", LOGICAL.AND), new ConditionExpression("record", "contains", "example.com", LOGICAL.AND),
]); ]);
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" })); assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" }));
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." })); assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." }));
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" })); assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" }));
}); });
test("Test evaluateExpressionGroup with logical OR", async (t) => { test("evaluateExpressionGroup() with OR logic requires at least one condition to be true", () => {
const group = new ConditionExpressionGroup([ const group = new ConditionExpressionGroup([
new ConditionExpression("record", "contains", "example.com"),
new ConditionExpression("record", "contains", "example.org", LOGICAL.OR),
]);
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.com" }));
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.org" }));
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.net" }));
});
test("Test evaluateExpressionGroup with nested group", async (t) => {
const group = new ConditionExpressionGroup([
new ConditionExpression("record", "contains", "mx1."),
new ConditionExpressionGroup([
new ConditionExpression("record", "contains", "example.com"), new ConditionExpression("record", "contains", "example.com"),
new ConditionExpression("record", "contains", "example.org", LOGICAL.OR), new ConditionExpression("record", "contains", "example.org", LOGICAL.OR),
]), ]);
]); assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.com" }));
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." })); assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.org" }));
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" })); assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.net" }));
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.org" })); });
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" }));
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.org" })); test("evaluateExpressionGroup() evaluates nested groups correctly", () => {
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1.example.net" })); const group = new ConditionExpressionGroup([
new ConditionExpression("record", "contains", "mx1."),
new ConditionExpressionGroup([
new ConditionExpression("record", "contains", "example.com"),
new ConditionExpression("record", "contains", "example.org", LOGICAL.OR),
]),
]);
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." }));
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" }));
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.org" }));
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" }));
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.org" }));
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1.example.net" }));
});
}); });
@@ -1,108 +1,110 @@
const test = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { operatorMap, OP_CONTAINS, OP_NOT_CONTAINS, OP_LT, OP_GT, OP_LTE, OP_GTE, OP_STR_EQUALS, OP_STR_NOT_EQUALS, OP_NUM_EQUALS, OP_NUM_NOT_EQUALS, OP_STARTS_WITH, OP_ENDS_WITH, OP_NOT_STARTS_WITH, OP_NOT_ENDS_WITH } = require("../../../server/monitor-conditions/operators.js"); const { operatorMap, OP_CONTAINS, OP_NOT_CONTAINS, OP_LT, OP_GT, OP_LTE, OP_GTE, OP_STR_EQUALS, OP_STR_NOT_EQUALS, OP_NUM_EQUALS, OP_NUM_NOT_EQUALS, OP_STARTS_WITH, OP_ENDS_WITH, OP_NOT_STARTS_WITH, OP_NOT_ENDS_WITH } = require("../../../server/monitor-conditions/operators.js");
test("Test StringEqualsOperator", async (t) => { describe("Expression Operators", () => {
const op = operatorMap.get(OP_STR_EQUALS); test("StringEqualsOperator returns true for identical strings and false otherwise", () => {
assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com")); const op = operatorMap.get(OP_STR_EQUALS);
assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.org")); assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com"));
assert.strictEqual(false, op.test("1", 1)); // strict equality assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.org"));
}); assert.strictEqual(false, op.test("1", 1)); // strict equality
});
test("Test StringNotEqualsOperator", async (t) => { test("StringNotEqualsOperator returns true for different strings and false for identical strings", () => {
const op = operatorMap.get(OP_STR_NOT_EQUALS); const op = operatorMap.get(OP_STR_NOT_EQUALS);
assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org")); assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org"));
assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com")); assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com"));
assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality) assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality)
}); });
test("Test ContainsOperator with scalar", async (t) => { test("ContainsOperator returns true when scalar contains substring", () => {
const op = operatorMap.get(OP_CONTAINS); const op = operatorMap.get(OP_CONTAINS);
assert.strictEqual(true, op.test("mx1.example.org", "example.org")); assert.strictEqual(true, op.test("mx1.example.org", "example.org"));
assert.strictEqual(false, op.test("mx1.example.org", "example.com")); assert.strictEqual(false, op.test("mx1.example.org", "example.com"));
}); });
test("Test ContainsOperator with array", async (t) => { test("ContainsOperator returns true when array contains element", () => {
const op = operatorMap.get(OP_CONTAINS); const op = operatorMap.get(OP_CONTAINS);
assert.strictEqual(true, op.test([ "example.org" ], "example.org")); assert.strictEqual(true, op.test([ "example.org" ], "example.org"));
assert.strictEqual(false, op.test([ "example.org" ], "example.com")); assert.strictEqual(false, op.test([ "example.org" ], "example.com"));
}); });
test("Test NotContainsOperator with scalar", async (t) => { test("NotContainsOperator returns true when scalar does not contain substring", () => {
const op = operatorMap.get(OP_NOT_CONTAINS); const op = operatorMap.get(OP_NOT_CONTAINS);
assert.strictEqual(true, op.test("example.org", ".com")); assert.strictEqual(true, op.test("example.org", ".com"));
assert.strictEqual(false, op.test("example.org", ".org")); assert.strictEqual(false, op.test("example.org", ".org"));
}); });
test("Test NotContainsOperator with array", async (t) => { test("NotContainsOperator returns true when array does not contain element", () => {
const op = operatorMap.get(OP_NOT_CONTAINS); const op = operatorMap.get(OP_NOT_CONTAINS);
assert.strictEqual(true, op.test([ "example.org" ], "example.com")); assert.strictEqual(true, op.test([ "example.org" ], "example.com"));
assert.strictEqual(false, op.test([ "example.org" ], "example.org")); assert.strictEqual(false, op.test([ "example.org" ], "example.org"));
}); });
test("Test StartsWithOperator", async (t) => { test("StartsWithOperator returns true when string starts with prefix", () => {
const op = operatorMap.get(OP_STARTS_WITH); const op = operatorMap.get(OP_STARTS_WITH);
assert.strictEqual(true, op.test("mx1.example.com", "mx1")); assert.strictEqual(true, op.test("mx1.example.com", "mx1"));
assert.strictEqual(false, op.test("mx1.example.com", "mx2")); assert.strictEqual(false, op.test("mx1.example.com", "mx2"));
}); });
test("Test NotStartsWithOperator", async (t) => { test("NotStartsWithOperator returns true when string does not start with prefix", () => {
const op = operatorMap.get(OP_NOT_STARTS_WITH); const op = operatorMap.get(OP_NOT_STARTS_WITH);
assert.strictEqual(true, op.test("mx1.example.com", "mx2")); assert.strictEqual(true, op.test("mx1.example.com", "mx2"));
assert.strictEqual(false, op.test("mx1.example.com", "mx1")); assert.strictEqual(false, op.test("mx1.example.com", "mx1"));
}); });
test("Test EndsWithOperator", async (t) => { test("EndsWithOperator returns true when string ends with suffix", () => {
const op = operatorMap.get(OP_ENDS_WITH); const op = operatorMap.get(OP_ENDS_WITH);
assert.strictEqual(true, op.test("mx1.example.com", "example.com")); assert.strictEqual(true, op.test("mx1.example.com", "example.com"));
assert.strictEqual(false, op.test("mx1.example.com", "example.net")); assert.strictEqual(false, op.test("mx1.example.com", "example.net"));
}); });
test("Test NotEndsWithOperator", async (t) => { test("NotEndsWithOperator returns true when string does not end with suffix", () => {
const op = operatorMap.get(OP_NOT_ENDS_WITH); const op = operatorMap.get(OP_NOT_ENDS_WITH);
assert.strictEqual(true, op.test("mx1.example.com", "example.net")); assert.strictEqual(true, op.test("mx1.example.com", "example.net"));
assert.strictEqual(false, op.test("mx1.example.com", "example.com")); assert.strictEqual(false, op.test("mx1.example.com", "example.com"));
}); });
test("Test NumberEqualsOperator", async (t) => { test("NumberEqualsOperator returns true for equal numbers with type coercion", () => {
const op = operatorMap.get(OP_NUM_EQUALS); const op = operatorMap.get(OP_NUM_EQUALS);
assert.strictEqual(true, op.test(1, 1)); assert.strictEqual(true, op.test(1, 1));
assert.strictEqual(true, op.test(1, "1")); assert.strictEqual(true, op.test(1, "1"));
assert.strictEqual(false, op.test(1, "2")); assert.strictEqual(false, op.test(1, "2"));
}); });
test("Test NumberNotEqualsOperator", async (t) => { test("NumberNotEqualsOperator returns true for different numbers", () => {
const op = operatorMap.get(OP_NUM_NOT_EQUALS); const op = operatorMap.get(OP_NUM_NOT_EQUALS);
assert.strictEqual(true, op.test(1, "2")); assert.strictEqual(true, op.test(1, "2"));
assert.strictEqual(false, op.test(1, "1")); assert.strictEqual(false, op.test(1, "1"));
}); });
test("Test LessThanOperator", async (t) => { test("LessThanOperator returns true when first number is less than second", () => {
const op = operatorMap.get(OP_LT); const op = operatorMap.get(OP_LT);
assert.strictEqual(true, op.test(1, 2)); assert.strictEqual(true, op.test(1, 2));
assert.strictEqual(true, op.test(1, "2")); assert.strictEqual(true, op.test(1, "2"));
assert.strictEqual(false, op.test(1, 1)); assert.strictEqual(false, op.test(1, 1));
}); });
test("Test GreaterThanOperator", async (t) => { test("GreaterThanOperator returns true when first number is greater than second", () => {
const op = operatorMap.get(OP_GT); const op = operatorMap.get(OP_GT);
assert.strictEqual(true, op.test(2, 1)); assert.strictEqual(true, op.test(2, 1));
assert.strictEqual(true, op.test(2, "1")); assert.strictEqual(true, op.test(2, "1"));
assert.strictEqual(false, op.test(1, 1)); assert.strictEqual(false, op.test(1, 1));
}); });
test("Test LessThanOrEqualToOperator", async (t) => { test("LessThanOrEqualToOperator returns true when first number is less than or equal to second", () => {
const op = operatorMap.get(OP_LTE); const op = operatorMap.get(OP_LTE);
assert.strictEqual(true, op.test(1, 1)); assert.strictEqual(true, op.test(1, 1));
assert.strictEqual(true, op.test(1, 2)); assert.strictEqual(true, op.test(1, 2));
assert.strictEqual(true, op.test(1, "2")); assert.strictEqual(true, op.test(1, "2"));
assert.strictEqual(false, op.test(1, 0)); assert.strictEqual(false, op.test(1, 0));
}); });
test("Test GreaterThanOrEqualToOperator", async (t) => { test("GreaterThanOrEqualToOperator returns true when first number is greater than or equal to second", () => {
const op = operatorMap.get(OP_GTE); const op = operatorMap.get(OP_GTE);
assert.strictEqual(true, op.test(1, 1)); assert.strictEqual(true, op.test(1, 1));
assert.strictEqual(true, op.test(2, 1)); assert.strictEqual(true, op.test(2, 1));
assert.strictEqual(true, op.test(2, "2")); assert.strictEqual(true, op.test(2, "2"));
assert.strictEqual(false, op.test(2, 3)); assert.strictEqual(false, op.test(2, 3));
});
}); });
@@ -2,8 +2,8 @@ const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const grpc = require("@grpc/grpc-js"); const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader"); const protoLoader = require("@grpc/proto-loader");
const { GrpcKeywordMonitorType } = require("../../server/monitor-types/grpc"); const { GrpcKeywordMonitorType } = require("../../../server/monitor-types/grpc");
const { UP, PENDING } = require("../../src/util"); const { UP, PENDING } = require("../../../src/util");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const os = require("os"); const os = require("os");
@@ -82,7 +82,7 @@ async function createTestGrpcServer(port, methodHandlers) {
describe("GrpcKeywordMonitorType", { describe("GrpcKeywordMonitorType", {
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"), skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
}, () => { }, () => {
test("gRPC keyword found in response", async () => { test("check() sets status to UP when keyword is found in response", async () => {
const port = 50051; const port = 50051;
const server = await createTestGrpcServer(port, { const server = await createTestGrpcServer(port, {
Echo: (call, callback) => { Echo: (call, callback) => {
@@ -118,7 +118,7 @@ describe("GrpcKeywordMonitorType", {
} }
}); });
test("gRPC keyword not found in response", async () => { test("check() rejects when keyword is not found in response", async () => {
const port = 50052; const port = 50052;
const server = await createTestGrpcServer(port, { const server = await createTestGrpcServer(port, {
Echo: (call, callback) => { Echo: (call, callback) => {
@@ -158,7 +158,7 @@ describe("GrpcKeywordMonitorType", {
} }
}); });
test("gRPC inverted keyword - keyword present (should fail)", async () => { test("check() rejects when inverted keyword is present in response", async () => {
const port = 50053; const port = 50053;
const server = await createTestGrpcServer(port, { const server = await createTestGrpcServer(port, {
Echo: (call, callback) => { Echo: (call, callback) => {
@@ -198,7 +198,7 @@ describe("GrpcKeywordMonitorType", {
} }
}); });
test("gRPC inverted keyword - keyword not present (should pass)", async () => { test("check() sets status to UP when inverted keyword is not present in response", async () => {
const port = 50054; const port = 50054;
const server = await createTestGrpcServer(port, { const server = await createTestGrpcServer(port, {
Echo: (call, callback) => { Echo: (call, callback) => {
@@ -234,7 +234,7 @@ describe("GrpcKeywordMonitorType", {
} }
}); });
test("gRPC connection failure", async () => { test("check() rejects when gRPC server is unreachable", async () => {
const grpcMonitor = new GrpcKeywordMonitorType(); const grpcMonitor = new GrpcKeywordMonitorType();
const monitor = { const monitor = {
grpcUrl: "localhost:50099", grpcUrl: "localhost:50099",
@@ -262,7 +262,7 @@ describe("GrpcKeywordMonitorType", {
); );
}); });
test("gRPC response truncation for long messages", async () => { test("check() truncates long response messages in error output", async () => {
const port = 50055; const port = 50055;
const longMessage = "A".repeat(100) + " with SUCCESS keyword"; const longMessage = "A".repeat(100) + " with SUCCESS keyword";
@@ -2,8 +2,8 @@ const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { HiveMQContainer } = require("@testcontainers/hivemq"); const { HiveMQContainer } = require("@testcontainers/hivemq");
const mqtt = require("mqtt"); const mqtt = require("mqtt");
const { MqttMonitorType } = require("../../server/monitor-types/mqtt"); const { MqttMonitorType } = require("../../../server/monitor-types/mqtt");
const { UP, PENDING } = require("../../src/util"); const { UP, PENDING } = require("../../../src/util");
/** /**
* Runs an MQTT test with the * Runs an MQTT test with the
@@ -58,91 +58,91 @@ describe("MqttMonitorType", {
concurrency: 4, concurrency: 4,
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64") skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64")
}, () => { }, () => {
test("valid keywords (type=default)", async () => { test("check() sets status to UP when keyword is found in message (type=default)", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-"); const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-"); assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
}); });
test("valid nested topic", async () => { test("check() sets status to UP when keyword is found in nested topic", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/b/c", "a/b/c"); const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/b/c", "a/b/c");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-"); assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-");
}); });
test("valid nested topic (with special chars)", async () => { test("check() sets status to UP when keyword is found in nested topic with special characters", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/'/$/./*/%", "a/'/$/./*/%"); const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/'/$/./*/%", "a/'/$/./*/%");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: a/'/$/./*/%; Message: -> KEYWORD <-"); assert.strictEqual(heartbeat.msg, "Topic: a/'/$/./*/%; Message: -> KEYWORD <-");
}); });
test("valid wildcard topic (with #)", async () => { test("check() sets status to UP when keyword is found using # wildcard", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/#", "a/b/c"); const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/#", "a/b/c");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-"); assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-");
}); });
test("valid wildcard topic (with +)", async () => { test("check() sets status to UP when keyword is found using + wildcard", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/+/c", "a/b/c"); const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/+/c", "a/b/c");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-"); assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-");
}); });
test("valid wildcard topic (with + and #)", async () => { test("check() sets status to UP when keyword is found using + and # wildcards", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/+/c/#", "a/b/c/d/e"); const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/+/c/#", "a/b/c/d/e");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: a/b/c/d/e; Message: -> KEYWORD <-"); assert.strictEqual(heartbeat.msg, "Topic: a/b/c/d/e; Message: -> KEYWORD <-");
}); });
test("invalid topic", async () => { test("check() rejects with timeout when topic does not match", async () => {
await assert.rejects( await assert.rejects(
testMqtt("keyword will not be checked anyway", null, "message", "x/y/z", "a/b/c"), testMqtt("keyword will not be checked anyway", null, "message", "x/y/z", "a/b/c"),
new Error("Timeout, Message not received"), new Error("Timeout, Message not received"),
); );
}); });
test("invalid wildcard topic (with #)", async () => { test("check() rejects with timeout when # wildcard is not last character", async () => {
await assert.rejects( await assert.rejects(
testMqtt("", null, "# should be last character", "#/c", "a/b/c"), testMqtt("", null, "# should be last character", "#/c", "a/b/c"),
new Error("Timeout, Message not received"), new Error("Timeout, Message not received"),
); );
}); });
test("invalid wildcard topic (with +)", async () => { test("check() rejects with timeout when + wildcard topic does not match", async () => {
await assert.rejects( await assert.rejects(
testMqtt("", null, "message", "x/+/z", "a/b/c"), testMqtt("", null, "message", "x/+/z", "a/b/c"),
new Error("Timeout, Message not received"), new Error("Timeout, Message not received"),
); );
}); });
test("valid keywords (type=keyword)", async () => { test("check() sets status to UP when keyword is found in message (type=keyword)", async () => {
const heartbeat = await testMqtt("KEYWORD", "keyword", "-> KEYWORD <-"); const heartbeat = await testMqtt("KEYWORD", "keyword", "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-"); assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
}); });
test("invalid keywords (type=default)", async () => { test("check() rejects when keyword is not found in message (type=default)", async () => {
await assert.rejects( await assert.rejects(
testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"), testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"), new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
); );
}); });
test("invalid keyword (type=keyword)", async () => { test("check() rejects when keyword is not found in message (type=keyword)", async () => {
await assert.rejects( await assert.rejects(
testMqtt("NOT_PRESENT", "keyword", "-> KEYWORD <-"), testMqtt("NOT_PRESENT", "keyword", "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"), new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
); );
}); });
test("valid json-query", async () => { test("check() sets status to UP when json-query finds expected value", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp" // works because the monitors' jsonPath is hard-coded to "firstProp"
const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}"); const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Message received, expected value is found"); assert.strictEqual(heartbeat.msg, "Message received, expected value is found");
}); });
test("invalid (because query fails) json-query", async () => { test("check() rejects when json-query path returns undefined", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp" // works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects( await assert.rejects(
testMqtt("[not_relevant]", "json-query", "{}"), testMqtt("[not_relevant]", "json-query", "{}"),
@@ -150,7 +150,7 @@ describe("MqttMonitorType", {
); );
}); });
test("invalid (because successMessage fails) json-query", async () => { test("check() rejects when json-query value does not match expected value", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp" // works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects( await assert.rejects(
testMqtt("[wrong_success_messsage]", "json-query", "{\"firstProp\":\"present\"}"), testMqtt("[wrong_success_messsage]", "json-query", "{\"firstProp\":\"present\"}"),
+302
View File
@@ -0,0 +1,302 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
const { MSSQLServerContainer } = require("@testcontainers/mssqlserver");
const { MssqlMonitorType } = require("../../../server/monitor-types/mssql");
const { UP, PENDING } = require("../../../src/util");
/**
* Helper function to create and start a MSSQL container
* @returns {Promise<MSSQLServerContainer>} The started MSSQL container
*/
async function createAndStartMSSQLContainer() {
return await new MSSQLServerContainer(
"mcr.microsoft.com/mssql/server:2022-latest"
)
.acceptLicense()
// The default timeout of 30 seconds might not be enough for the container to start
.withStartupTimeout(60000)
.start();
}
describe(
"MSSQL Single Node",
{
skip:
!!process.env.CI &&
(process.platform !== "linux" || process.arch !== "x64"),
},
() => {
test("check() sets status to UP when MSSQL server is reachable", async () => {
let mssqlContainer;
try {
mssqlContainer = await createAndStartMSSQLContainer();
const mssqlMonitor = new MssqlMonitorType();
const monitor = {
databaseConnectionString:
mssqlContainer.getConnectionUri(false),
conditions: "[]",
};
const heartbeat = {
msg: "",
status: PENDING,
};
await mssqlMonitor.check(monitor, heartbeat, {});
assert.strictEqual(
heartbeat.status,
UP,
`Expected status ${UP} but got ${heartbeat.status}`
);
} catch (error) {
console.error("Test failed with error:", error.message);
console.error("Error stack:", error.stack);
if (mssqlContainer) {
console.error("Container ID:", mssqlContainer.getId());
console.error(
"Container logs:",
await mssqlContainer.logs()
);
}
throw error;
} finally {
if (mssqlContainer) {
console.log("Stopping MSSQL container...");
await mssqlContainer.stop();
}
}
});
test("check() sets status to UP when custom query returns single value", async () => {
const mssqlContainer = await createAndStartMSSQLContainer();
const mssqlMonitor = new MssqlMonitorType();
const monitor = {
databaseConnectionString:
mssqlContainer.getConnectionUri(false),
databaseQuery: "SELECT 42",
conditions: "[]",
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await mssqlMonitor.check(monitor, heartbeat, {});
assert.strictEqual(
heartbeat.status,
UP,
`Expected status ${UP} but got ${heartbeat.status}`
);
} finally {
await mssqlContainer.stop();
}
});
test("check() sets status to UP when custom query result meets condition", async () => {
const mssqlContainer = await createAndStartMSSQLContainer();
const mssqlMonitor = new MssqlMonitorType();
const monitor = {
databaseConnectionString:
mssqlContainer.getConnectionUri(false),
databaseQuery: "SELECT 42 as value",
conditions: JSON.stringify([
{
type: "expression",
andOr: "and",
variable: "result",
operator: "equals",
value: "42",
},
]),
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await mssqlMonitor.check(monitor, heartbeat, {});
assert.strictEqual(
heartbeat.status,
UP,
`Expected status ${UP} but got ${heartbeat.status}`
);
} finally {
await mssqlContainer.stop();
}
});
test("check() rejects when custom query result does not meet condition", async () => {
const mssqlContainer = await createAndStartMSSQLContainer();
const mssqlMonitor = new MssqlMonitorType();
const monitor = {
databaseConnectionString:
mssqlContainer.getConnectionUri(false),
databaseQuery: "SELECT 99 as value",
conditions: JSON.stringify([
{
type: "expression",
andOr: "and",
variable: "result",
operator: "equals",
value: "42",
},
]),
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await assert.rejects(
mssqlMonitor.check(monitor, heartbeat, {}),
new Error(
"Query result did not meet the specified conditions (99)"
)
);
assert.strictEqual(
heartbeat.status,
PENDING,
`Expected status should not be ${heartbeat.status}`
);
} finally {
await mssqlContainer.stop();
}
});
test("check() rejects when query returns no results", async () => {
const mssqlContainer = await createAndStartMSSQLContainer();
const mssqlMonitor = new MssqlMonitorType();
const monitor = {
databaseConnectionString:
mssqlContainer.getConnectionUri(false),
databaseQuery: "SELECT 1 WHERE 1 = 0",
conditions: "[]",
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await assert.rejects(
mssqlMonitor.check(monitor, heartbeat, {}),
new Error(
"Database connection/query failed: Query returned no results"
)
);
assert.strictEqual(
heartbeat.status,
PENDING,
`Expected status should not be ${heartbeat.status}`
);
} finally {
await mssqlContainer.stop();
}
});
test("check() rejects when query returns multiple rows", async () => {
const mssqlContainer = await createAndStartMSSQLContainer();
const mssqlMonitor = new MssqlMonitorType();
const monitor = {
databaseConnectionString:
mssqlContainer.getConnectionUri(false),
databaseQuery: "SELECT 1 UNION ALL SELECT 2",
conditions: "[]",
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await assert.rejects(
mssqlMonitor.check(monitor, heartbeat, {}),
new Error(
"Database connection/query failed: Multiple values were found, expected only one value"
)
);
assert.strictEqual(
heartbeat.status,
PENDING,
`Expected status should not be ${heartbeat.status}`
);
} finally {
await mssqlContainer.stop();
}
});
test("check() rejects when query returns multiple columns", async () => {
const mssqlContainer = await createAndStartMSSQLContainer();
const mssqlMonitor = new MssqlMonitorType();
const monitor = {
databaseConnectionString:
mssqlContainer.getConnectionUri(false),
databaseQuery: "SELECT 1 AS col1, 2 AS col2",
conditions: "[]",
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await assert.rejects(
mssqlMonitor.check(monitor, heartbeat, {}),
new Error(
"Database connection/query failed: Multiple columns were found, expected only one value"
)
);
assert.strictEqual(
heartbeat.status,
PENDING,
`Expected status should not be ${heartbeat.status}`
);
} finally {
await mssqlContainer.stop();
}
});
test("check() rejects when MSSQL server is not reachable", async () => {
const mssqlMonitor = new MssqlMonitorType();
const monitor = {
databaseConnectionString:
"Server=localhost,15433;Database=master;User Id=Fail;Password=Fail;Encrypt=false",
conditions: "[]",
};
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(
mssqlMonitor.check(monitor, heartbeat, {}),
new Error(
"Database connection/query failed: Failed to connect to localhost:15433 - Could not connect (sequence)"
)
);
assert.notStrictEqual(
heartbeat.status,
UP,
`Expected status should not be ${heartbeat.status}`
);
});
}
);
@@ -1,8 +1,8 @@
const { describe, test } = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { PostgreSqlContainer } = require("@testcontainers/postgresql"); const { PostgreSqlContainer } = require("@testcontainers/postgresql");
const { PostgresMonitorType } = require("../../server/monitor-types/postgres"); const { PostgresMonitorType } = require("../../../server/monitor-types/postgres");
const { UP, PENDING } = require("../../src/util"); const { UP, PENDING } = require("../../../src/util");
describe( describe(
"Postgres Single Node", "Postgres Single Node",
@@ -12,7 +12,7 @@ describe(
(process.platform !== "linux" || process.arch !== "x64"), (process.platform !== "linux" || process.arch !== "x64"),
}, },
() => { () => {
test("Postgres is running", async () => { test("check() sets status to UP when Postgres server is reachable", async () => {
// The default timeout of 30 seconds might not be enough for the container to start // The default timeout of 30 seconds might not be enough for the container to start
const postgresContainer = await new PostgreSqlContainer( const postgresContainer = await new PostgreSqlContainer(
"postgres:latest" "postgres:latest"
@@ -37,7 +37,7 @@ describe(
} }
}); });
test("Postgres is not running", async () => { test("check() rejects when Postgres server is not reachable", async () => {
const postgresMonitor = new PostgresMonitorType(); const postgresMonitor = new PostgresMonitorType();
const monitor = { const monitor = {
databaseConnectionString: "http://localhost:15432", databaseConnectionString: "http://localhost:15432",
@@ -1,13 +1,13 @@
const { describe, test } = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { RabbitMQContainer } = require("@testcontainers/rabbitmq"); const { RabbitMQContainer } = require("@testcontainers/rabbitmq");
const { RabbitMqMonitorType } = require("../../server/monitor-types/rabbitmq"); const { RabbitMqMonitorType } = require("../../../server/monitor-types/rabbitmq");
const { UP, PENDING } = require("../../src/util"); const { UP, PENDING } = require("../../../src/util");
describe("RabbitMQ Single Node", { describe("RabbitMQ Single Node", {
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"), skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
}, () => { }, () => {
test("RabbitMQ is running", async () => { test("check() sets status to UP when RabbitMQ server is reachable", async () => {
// The default timeout of 30 seconds might not be enough for the container to start // The default timeout of 30 seconds might not be enough for the container to start
const rabbitMQContainer = await new RabbitMQContainer().withStartupTimeout(60000).start(); const rabbitMQContainer = await new RabbitMQContainer().withStartupTimeout(60000).start();
const rabbitMQMonitor = new RabbitMqMonitorType(); const rabbitMQMonitor = new RabbitMqMonitorType();
@@ -33,7 +33,7 @@ describe("RabbitMQ Single Node", {
} }
}); });
test("RabbitMQ is not running", async () => { test("check() rejects when RabbitMQ server is not reachable", async () => {
const rabbitMQMonitor = new RabbitMqMonitorType(); const rabbitMQMonitor = new RabbitMqMonitorType();
const monitor = { const monitor = {
rabbitmqNodes: JSON.stringify([ "http://localhost:15672" ]), rabbitmqNodes: JSON.stringify([ "http://localhost:15672" ]),
@@ -1,14 +1,9 @@
const { describe, test } = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { TCPMonitorType } = require("../../server/monitor-types/tcp"); const { TCPMonitorType } = require("../../../server/monitor-types/tcp");
const { UP, PENDING } = require("../../src/util"); const { UP, PENDING } = require("../../../src/util");
const net = require("net"); const net = require("net");
/**
* Test suite for TCP Monitor functionality
* This test suite checks the behavior of the TCPMonitorType class
* under different network connection scenarios.
*/
describe("TCP Monitor", () => { describe("TCP Monitor", () => {
/** /**
* Creates a TCP server on a specified port * Creates a TCP server on a specified port
@@ -29,11 +24,7 @@ describe("TCP Monitor", () => {
}); });
} }
/** test("check() sets status to UP when TCP server is reachable", async () => {
* Test case to verify TCP monitor works when a server is running
* Checks that the monitor correctly identifies an active TCP server
*/
test("TCP server is running", async () => {
const port = 12345; const port = 12345;
const server = await createTCPServer(port); const server = await createTCPServer(port);
@@ -59,11 +50,7 @@ describe("TCP Monitor", () => {
} }
}); });
/** test("check() rejects with connection failed when TCP server is not running", async () => {
* Test case to verify TCP monitor handles non-running servers
* Checks that the monitor correctly identifies an inactive TCP server
*/
test("TCP server is not running", async () => {
const tcpMonitor = new TCPMonitorType(); const tcpMonitor = new TCPMonitorType();
const monitor = { const monitor = {
@@ -83,11 +70,7 @@ describe("TCP Monitor", () => {
); );
}); });
/** test("check() rejects when TLS certificate is expired or invalid", async () => {
* Test case to verify TCP monitor handles servers with expired or invalid TLS certificates
* Checks that the monitor correctly identifies TLS certificate issues
*/
test("TCP server with expired or invalid TLS certificate", async t => {
const tcpMonitor = new TCPMonitorType(); const tcpMonitor = new TCPMonitorType();
const monitor = { const monitor = {
@@ -114,7 +97,7 @@ describe("TCP Monitor", () => {
); );
}); });
test("TCP server with valid TLS certificate (SSL)", async t => { test("check() sets status to UP when TLS certificate is valid (SSL)", async () => {
const tcpMonitor = new TCPMonitorType(); const tcpMonitor = new TCPMonitorType();
const monitor = { const monitor = {
@@ -137,7 +120,7 @@ describe("TCP Monitor", () => {
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
}); });
test("TCP server with valid TLS certificate (STARTTLS)", async t => { test("check() sets status to UP when TLS certificate is valid (STARTTLS)", async () => {
const tcpMonitor = new TCPMonitorType(); const tcpMonitor = new TCPMonitorType();
const monitor = { const monitor = {
@@ -160,7 +143,7 @@ describe("TCP Monitor", () => {
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
}); });
test("TCP server with valid but name mismatching TLS certificate (STARTTLS)", async t => { test("check() rejects when TLS certificate hostname does not match (STARTTLS)", async () => {
const tcpMonitor = new TCPMonitorType(); const tcpMonitor = new TCPMonitorType();
const monitor = { const monitor = {
@@ -185,7 +168,7 @@ describe("TCP Monitor", () => {
regex regex
); );
}); });
test("XMPP server with valid certificate (STARTTLS)", async t => { test("check() sets status to UP for XMPP server with valid certificate (STARTTLS)", async () => {
const tcpMonitor = new TCPMonitorType(); const tcpMonitor = new TCPMonitorType();
const monitor = { const monitor = {
@@ -0,0 +1,338 @@
const { WebSocketServer } = require("ws");
const { describe, test } = require("node:test");
const assert = require("node:assert");
const { WebSocketMonitorType } = require("../../../server/monitor-types/websocket-upgrade");
const { UP, PENDING } = require("../../../src/util");
const net = require("node:net");
/**
* Simulates non compliant WS Server, doesnt send Sec-WebSocket-Accept header
* @param {number} port Port the server listens on. Defaults to 8080
* @returns {Promise} Promise that resolves to the created server once listening
*/
function nonCompliantWS(port = 8080) {
const srv = net.createServer((socket) => {
socket.once("data", (buf) => {
socket.write("HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n\r\n");
socket.destroy();
});
});
return new Promise((resolve) => srv.listen(port, () => resolve(srv)));
}
describe("WebSocket Monitor", {
}, () => {
test("check() rejects with unexpected server response when connecting to non-WebSocket server", {}, async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://example.org",
wsIgnoreSecWebsocketAcceptHeader: false,
timeout: 30,
};
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new Error("Unexpected server response: 200")
);
});
test("check() sets status to UP when connecting to secure WebSocket server", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://echo.websocket.org",
wsIgnoreSecWebsocketAcceptHeader: false,
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
timeout: 30,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "1000 - OK",
status: UP,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
test("check() sets status to UP when connecting to insecure WebSocket server", async (t) => {
t.after(() => wss.close());
const websocketMonitor = new WebSocketMonitorType();
const wss = new WebSocketServer({ port: 8080 });
const monitor = {
url: "ws://localhost:8080",
wsIgnoreSecWebsocketAcceptHeader: false,
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
timeout: 30,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "1000 - OK",
status: UP,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
test("check() rejects when status code does not match expected value", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://echo.websocket.org",
wsIgnoreSecWebsocketAcceptHeader: false,
accepted_statuscodes_json: JSON.stringify([ "1001" ]),
timeout: 30,
};
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new Error("Unexpected status code: 1000")
);
});
test("check() rejects when expected status code is empty", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://echo.websocket.org",
wsIgnoreSecWebsocketAcceptHeader: false,
accepted_statuscodes_json: JSON.stringify([ "" ]),
timeout: 30,
};
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new Error("Unexpected status code: 1000")
);
});
test("check() rejects when Sec-WebSocket-Accept header is invalid", async (t) => {
t.after(() => wss.close());
const websocketMonitor = new WebSocketMonitorType();
const wss = await nonCompliantWS();
const monitor = {
url: "ws://localhost:8080",
wsIgnoreSecWebsocketAcceptHeader: false,
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
timeout: 30,
};
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new Error("Invalid Sec-WebSocket-Accept header")
);
});
test("check() sets status to UP when ignoring invalid Sec-WebSocket-Accept header", async (t) => {
t.after(() => wss.close());
const websocketMonitor = new WebSocketMonitorType();
const wss = await nonCompliantWS();
const monitor = {
url: "ws://localhost:8080",
wsIgnoreSecWebsocketAcceptHeader: true,
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
timeout: 30,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "1000 - OK",
status: UP,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
test("check() sets status to UP for compliant WebSocket server when ignoring Sec-WebSocket-Accept", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://echo.websocket.org",
wsIgnoreSecWebsocketAcceptHeader: true,
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
timeout: 30,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "1000 - OK",
status: UP,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
test("check() rejects non-WebSocket server even when ignoring Sec-WebSocket-Accept", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://example.org",
wsIgnoreSecWebsocketAcceptHeader: true,
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
timeout: 30,
};
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new Error("Unexpected server response: 200")
);
});
test("check() rejects when server does not support requested subprotocol", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://echo.websocket.org",
wsIgnoreSecWebsocketAcceptHeader: false,
wsSubprotocol: "ocpp1.6",
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
timeout: 30,
};
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new Error("Server sent no subprotocol")
);
});
test("check() rejects when multiple subprotocols contain invalid characters", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://echo.websocket.org",
wsIgnoreSecWebsocketAcceptHeader: false,
wsSubprotocol: " # & ,ocpp2.0 [] , ocpp1.6 , ,, ; ",
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
timeout: 30,
};
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new SyntaxError("An invalid or duplicated subprotocol was specified")
);
});
test("check() sets status to UP when subprotocol with multiple spaces is accepted", async (t) => {
t.after(() => wss.close());
const websocketMonitor = new WebSocketMonitorType();
const wss = new WebSocketServer({ port: 8080,
handleProtocols: (protocols) => {
return Array.from(protocols).includes("test") ? "test" : null;
}
});
const monitor = {
url: "ws://localhost:8080",
wsIgnoreSecWebsocketAcceptHeader: false,
wsSubprotocol: "invalid , test ",
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
timeout: 30,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "1000 - OK",
status: UP,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
test("check() sets status to UP when server supports requested subprotocol", async (t) => {
t.after(() => wss.close());
const websocketMonitor = new WebSocketMonitorType();
const wss = new WebSocketServer({ port: 8080,
handleProtocols: (protocols) => {
return Array.from(protocols).includes("test") ? "test" : null;
}
});
const monitor = {
url: "ws://localhost:8080",
wsIgnoreSecWebsocketAcceptHeader: false,
wsSubprotocol: "invalid,test",
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
timeout: 30,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "1000 - OK",
status: UP,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
});
@@ -1,28 +1,28 @@
const express = require("express"); const express = require("express");
const bodyParser = require("body-parser"); const bodyParser = require("body-parser");
/** /**
* @param {number} port Port number * @param {number} port Port number
* @param {string} url Webhook URL * @param {string} url Webhook URL
* @param {number} timeout Timeout * @param {number} timeout Timeout
* @returns {Promise<object>} Webhook data * @returns {Promise<object>} Webhook data
*/ */
async function mockWebhook(port, url, timeout = 2500) { async function mockWebhook(port, url, timeout = 2500) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const app = express(); const app = express();
const tmo = setTimeout(() => { const tmo = setTimeout(() => {
server.close(); server.close();
reject({ reason: "Timeout" }); reject({ reason: "Timeout" });
}, timeout); }, timeout);
app.use(bodyParser.json()); // Middleware to parse JSON bodies app.use(bodyParser.json()); // Middleware to parse JSON bodies
app.post(`/${url}`, (req, res) => { app.post(`/${url}`, (req, res) => {
res.status(200).send("OK"); res.status(200).send("OK");
server.close(); server.close();
tmo && clearTimeout(tmo); tmo && clearTimeout(tmo);
resolve(req.body); resolve(req.body);
}); });
const server = app.listen(port); const server = app.listen(port);
}); });
} }
module.exports = mockWebhook; module.exports = mockWebhook;
+10 -8
View File
@@ -1,4 +1,4 @@
const { test } = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
@@ -36,12 +36,14 @@ iPenGDCg1awOyRnvxNq1MtMDkR9AHwksukzwiYNexYjyvE2t0UzXhFXwazQ3
-----END CERTIFICATE----- -----END CERTIFICATE-----
`; `;
test("Certificate and hostname match", () => { describe("Certificate Hostname Validation", () => {
const result = checkCertificateHostname(testCert, "www.eff.org"); test("checkCertificateHostname() returns true when certificate matches hostname", () => {
assert.strictEqual(result, true); const result = checkCertificateHostname(testCert, "www.eff.org");
}); assert.strictEqual(result, true);
});
test("Certificate and hostname mismatch", () => { test("checkCertificateHostname() returns false when certificate does not match hostname", () => {
const result = checkCertificateHostname(testCert, "example.com"); const result = checkCertificateHostname(testCert, "example.com");
assert.strictEqual(result, false); assert.strictEqual(result, false);
});
}); });
+22 -19
View File
@@ -1,9 +1,9 @@
process.env.UPTIME_KUMA_HIDE_LOG = [ "info_db", "info_server" ].join(","); process.env.UPTIME_KUMA_HIDE_LOG = [ "info_db", "info_server" ].join(",");
const test = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const DomainExpiry = require("../../server/model/domain_expiry"); const DomainExpiry = require("../../server/model/domain_expiry");
const mockWebhook = require("../mock-webhook"); const mockWebhook = require("./notification-providers/mock-webhook");
const TestDB = require("../mock-testdb"); const TestDB = require("../mock-testdb");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { Notification } = require("../../server/notification"); const { Notification } = require("../../server/notification");
@@ -12,30 +12,34 @@ const { setSetting } = require("../../server/util-server");
const testDb = new TestDB(); const testDb = new TestDB();
test("Domain Expiry", async (t) => { describe("Domain Expiry", () => {
await testDb.create();
Notification.init();
const monHttpCom = { const monHttpCom = {
type: "http", type: "http",
url: "https://www.google.com", url: "https://www.google.com",
domainExpiryNotification: true domainExpiryNotification: true
}; };
await t.test("Should get expiry date for .wiki with no A record", async () => {
test("getExpiryDate() returns correct expiry date for .wiki domain with no A record", async () => {
await testDb.create();
Notification.init();
const d = DomainExpiry.createByName("google.wiki"); const d = DomainExpiry.createByName("google.wiki");
assert.deepEqual(await d.getExpiryDate(), new Date("2026-11-26T23:59:59.000Z")); assert.deepEqual(await d.getExpiryDate(), new Date("2026-11-26T23:59:59.000Z"));
}); });
await t.test("Should get expiration date for .com from RDAP", async () => {
test("forMonitor() retrieves expiration date for .com domain from RDAP", async () => {
const domain = await DomainExpiry.forMonitor(monHttpCom); const domain = await DomainExpiry.forMonitor(monHttpCom);
const expiryFromRdap = await domain.getExpiryDate(); // from RDAP const expiryFromRdap = await domain.getExpiryDate(); // from RDAP
assert.deepEqual(expiryFromRdap, new Date("2028-09-14T04:00:00.000Z")); assert.deepEqual(expiryFromRdap, new Date("2028-09-14T04:00:00.000Z"));
}); });
await t.test("Should have expiration date cached in database", async () => {
test("checkExpiry() caches expiration date in database", async () => {
await DomainExpiry.checkExpiry(monHttpCom); // RDAP -> Cache await DomainExpiry.checkExpiry(monHttpCom); // RDAP -> Cache
const domain = await DomainExpiry.findByName("google.com"); const domain = await DomainExpiry.findByName("google.com");
assert(Date.now() - domain.lastCheck < 5 * 1000); assert(Date.now() - domain.lastCheck < 5 * 1000);
}); });
await t.test("Should trigger notify for expiring domain", async () => {
test("sendNotifications() triggers notification for expiring domain", async () => {
await DomainExpiry.findByName("google.com"); await DomainExpiry.findByName("google.com");
const hook = { const hook = {
"port": 3010, "port": 3010,
@@ -53,18 +57,17 @@ test("Domain Expiry", async (t) => {
"user_id": 1, "user_id": 1,
"name": "Testhook" "name": "Testhook"
}); });
const manyDays = 1500; const manyDays = 3650;
setSetting("domainExpiryNotifyDays", [ 7, 14, manyDays ], "general"); setSetting("domainExpiryNotifyDays", [ manyDays ], "general");
const [ notifRet, data ] = await Promise.all([ const [ , data ] = await Promise.all([
DomainExpiry.sendNotifications(monHttpCom, [ notif ]), DomainExpiry.sendNotifications(monHttpCom, [ notif ]),
mockWebhook(hook.port, hook.url) mockWebhook(hook.port, hook.url)
]); ]);
assert.equal(notifRet, manyDays);
assert.match(data.msg, /will expire in/); assert.match(data.msg, /will expire in/);
setTimeout(async () => {
Settings.stopCacheCleaner();
await testDb.destroy();
}, 200);
}); });
}).finally(() => {
setTimeout(async () => {
Settings.stopCacheCleaner();
await testDb.destroy();
}, 200);
}); });
-65
View File
@@ -1,65 +0,0 @@
const test = require("node:test");
const assert = require("node:assert");
const dayjs = require("dayjs");
const { SQL_DATETIME_FORMAT } = require("../../src/util");
dayjs.extend(require("dayjs/plugin/utc"));
dayjs.extend(require("dayjs/plugin/customParseFormat"));
/**
* Tests for maintenance date formatting to ensure compatibility with MariaDB/MySQL.
* Issue: MariaDB rejects ISO format dates like '2025-12-19T01:04:02.129Z'
* Fix: Use SQL_DATETIME_FORMAT ('YYYY-MM-DD HH:mm:ss') instead of toISOString()
*/
test("Maintenance Date Format - MariaDB Compatibility", async (t) => {
await t.test("SQL_DATETIME_FORMAT constant should match MariaDB format", async () => {
assert.strictEqual(SQL_DATETIME_FORMAT, "YYYY-MM-DD HH:mm:ss");
});
await t.test("Format date using SQL_DATETIME_FORMAT", async () => {
const current = dayjs.utc("2025-12-19T01:04:02.129Z");
const sqlFormat = current.utc().format(SQL_DATETIME_FORMAT);
assert.strictEqual(sqlFormat, "2025-12-19 01:04:02");
});
await t.test("SQL format should not contain ISO markers (T, Z)", async () => {
const current = dayjs.utc("2025-12-19T01:04:02.129Z");
const sqlFormat = current.utc().format(SQL_DATETIME_FORMAT);
assert.strictEqual(sqlFormat.includes("T"), false, "SQL format should not contain 'T'");
assert.strictEqual(sqlFormat.includes("Z"), false, "SQL format should not contain 'Z'");
});
await t.test("SQL format should match YYYY-MM-DD HH:mm:ss pattern", async () => {
const current = dayjs.utc("2025-12-19T01:04:02.129Z");
const sqlFormat = current.utc().format(SQL_DATETIME_FORMAT);
const sqlDateTimeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
assert.strictEqual(sqlDateTimeRegex.test(sqlFormat), true);
});
await t.test("Parse SQL datetime back to dayjs preserves timestamp", async () => {
const originalDate = dayjs.utc("2025-12-19T01:04:02.000Z");
const sqlFormat = originalDate.utc().format(SQL_DATETIME_FORMAT);
const parsedDate = dayjs.utc(sqlFormat, SQL_DATETIME_FORMAT);
assert.strictEqual(parsedDate.unix(), originalDate.unix());
});
await t.test("Edge case: midnight timestamp", async () => {
const midnight = dayjs.utc("2025-01-01T00:00:00.000Z");
const sqlFormat = midnight.utc().format(SQL_DATETIME_FORMAT);
assert.strictEqual(sqlFormat, "2025-01-01 00:00:00");
});
await t.test("Edge case: end of day timestamp", async () => {
const endOfDay = dayjs.utc("2025-12-31T23:59:59.999Z");
const sqlFormat = endOfDay.utc().format(SQL_DATETIME_FORMAT);
assert.strictEqual(sqlFormat, "2025-12-31 23:59:59");
});
});
+3 -3
View File
@@ -3,8 +3,8 @@ const fs = require("fs");
const path = require("path"); const path = require("path");
const { GenericContainer, Wait } = require("testcontainers"); const { GenericContainer, Wait } = require("testcontainers");
describe("Database Migration - Optimize Important Indexes", () => { describe("Database Migration", () => {
test("SQLite: All migrations run successfully", async () => { test("SQLite migrations run successfully from fresh database", async () => {
const testDbPath = path.join(__dirname, "../../data/test-migration.db"); const testDbPath = path.join(__dirname, "../../data/test-migration.db");
const testDbDir = path.dirname(testDbPath); const testDbDir = path.dirname(testDbPath);
@@ -57,7 +57,7 @@ describe("Database Migration - Optimize Important Indexes", () => {
}); });
test( test(
"MariaDB: All migrations run successfully", "MariaDB migrations run successfully from fresh database",
{ {
skip: skip:
!!process.env.CI && !!process.env.CI &&
+339 -341
View File
@@ -1,4 +1,4 @@
const test = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { UptimeCalculator } = require("../../server/uptime-calculator"); const { UptimeCalculator } = require("../../server/uptime-calculator");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
@@ -7,344 +7,403 @@ dayjs.extend(require("dayjs/plugin/utc"));
dayjs.extend(require("../../server/modules/dayjs/plugin/timezone")); dayjs.extend(require("../../server/modules/dayjs/plugin/timezone"));
dayjs.extend(require("dayjs/plugin/customParseFormat")); dayjs.extend(require("dayjs/plugin/customParseFormat"));
test("Test Uptime Calculator - custom date", async (t) => { describe("Uptime Calculator", () => {
let c1 = new UptimeCalculator(); test("getCurrentDate() returns custom date when set", () => {
let c1 = new UptimeCalculator();
// Test custom date // Test custom date
UptimeCalculator.currentDate = dayjs.utc("2021-01-01T00:00:00.000Z"); UptimeCalculator.currentDate = dayjs.utc("2021-01-01T00:00:00.000Z");
assert.strictEqual(c1.getCurrentDate().unix(), dayjs.utc("2021-01-01T00:00:00.000Z").unix()); assert.strictEqual(c1.getCurrentDate().unix(), dayjs.utc("2021-01-01T00:00:00.000Z").unix());
}); });
test("Test update - UP", async (t) => { test("update() with UP status returns correct timestamp", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let date = await c2.update(UP); let date = await c2.update(UP);
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:46:59").unix()); assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:46:59").unix());
}); });
test("Test update - MAINTENANCE", async (t) => { test("update() with MAINTENANCE status returns correct timestamp", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20"); UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let date = await c2.update(MAINTENANCE); let date = await c2.update(MAINTENANCE);
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix()); assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
}); });
test("Test update - DOWN", async (t) => { test("update() with DOWN status returns correct timestamp", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20"); UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let date = await c2.update(DOWN); let date = await c2.update(DOWN);
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix()); assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
}); });
test("Test update - PENDING", async (t) => { test("update() with PENDING status returns correct timestamp", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20"); UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let date = await c2.update(PENDING); let date = await c2.update(PENDING);
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix()); assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
}); });
test("Test flatStatus", async (t) => { test("flatStatus() converts statuses correctly", () => {
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
assert.strictEqual(c2.flatStatus(UP), UP); assert.strictEqual(c2.flatStatus(UP), UP);
//assert.strictEqual(c2.flatStatus(MAINTENANCE), UP); //assert.strictEqual(c2.flatStatus(MAINTENANCE), UP);
assert.strictEqual(c2.flatStatus(DOWN), DOWN); assert.strictEqual(c2.flatStatus(DOWN), DOWN);
assert.strictEqual(c2.flatStatus(PENDING), DOWN); assert.strictEqual(c2.flatStatus(PENDING), DOWN);
}); });
test("Test getMinutelyKey", async (t) => { test("getMinutelyKey() returns correct timestamp for start of minute", () => {
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:00")); let divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:00"));
assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix()); assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
// Edge case 1 // Edge case 1
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:01")); divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:01"));
assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix()); assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
// Edge case 2 // Edge case 2
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:59")); divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:59"));
assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix()); assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
}); });
test("Test getDailyKey", async (t) => { test("getDailyKey() returns correct timestamp for start of day", () => {
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 20:46:00")); let dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 20:46:00"));
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:45:30")); dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:45:30"));
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
// Edge case 1 // Edge case 1
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:59:59")); dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:59:59"));
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
// Edge case 2 // Edge case 2
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 00:00:00")); dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 00:00:00"));
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
// Test timezone // Test timezone
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
dailyKey = c2.getDailyKey(dayjs("Sat Dec 23 2023 05:38:39 GMT+0800 (Hong Kong Standard Time)")); dailyKey = c2.getDailyKey(dayjs("Sat Dec 23 2023 05:38:39 GMT+0800 (Hong Kong Standard Time)"));
assert.strictEqual(dailyKey, dayjs.utc("2023-12-22").unix()); assert.strictEqual(dailyKey, dayjs.utc("2023-12-22").unix());
}); });
test("Test lastDailyUptimeData", async (t) => { test("lastDailyUptimeData tracks UP status correctly", async () => {
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
await c2.update(UP); await c2.update(UP);
assert.strictEqual(c2.lastDailyUptimeData.up, 1); assert.strictEqual(c2.lastDailyUptimeData.up, 1);
}); });
test("Test get24Hour Uptime and Avg Ping", async (t) => { test("get24Hour() calculates uptime and average ping correctly", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
// No data // No data
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let data = c2.get24Hour(); let data = c2.get24Hour();
assert.strictEqual(data.uptime, 0); assert.strictEqual(data.uptime, 0);
assert.strictEqual(data.avgPing, null); assert.strictEqual(data.avgPing, null);
// 1 Up // 1 Up
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP, 100); await c2.update(UP, 100);
let uptime = c2.get24Hour().uptime; let uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 1); assert.strictEqual(uptime, 1);
assert.strictEqual(c2.get24Hour().avgPing, 100); assert.strictEqual(c2.get24Hour().avgPing, 100);
// 2 Up // 2 Up
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP, 100); await c2.update(UP, 100);
await c2.update(UP, 200); await c2.update(UP, 200);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 1); assert.strictEqual(uptime, 1);
assert.strictEqual(c2.get24Hour().avgPing, 150); assert.strictEqual(c2.get24Hour().avgPing, 150);
// 3 Up // 3 Up
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP, 0); await c2.update(UP, 0);
await c2.update(UP, 100); await c2.update(UP, 100);
await c2.update(UP, 400); await c2.update(UP, 400);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 1); assert.strictEqual(uptime, 1);
assert.strictEqual(c2.get24Hour().avgPing, 166.66666666666666); assert.strictEqual(c2.get24Hour().avgPing, 166.66666666666666);
// 1 MAINTENANCE // 1 MAINTENANCE
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(MAINTENANCE); await c2.update(MAINTENANCE);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
assert.strictEqual(c2.get24Hour().avgPing, null); assert.strictEqual(c2.get24Hour().avgPing, null);
// 1 PENDING // 1 PENDING
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(PENDING); await c2.update(PENDING);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
assert.strictEqual(c2.get24Hour().avgPing, null); assert.strictEqual(c2.get24Hour().avgPing, null);
// 1 DOWN // 1 DOWN
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
assert.strictEqual(c2.get24Hour().avgPing, null); assert.strictEqual(c2.get24Hour().avgPing, null);
// 2 DOWN // 2 DOWN
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(DOWN); await c2.update(DOWN);
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
assert.strictEqual(c2.get24Hour().avgPing, null); assert.strictEqual(c2.get24Hour().avgPing, null);
// 1 DOWN, 1 UP // 1 DOWN, 1 UP
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(DOWN); await c2.update(DOWN);
await c2.update(UP, 0.5); await c2.update(UP, 0.5);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0.5); assert.strictEqual(uptime, 0.5);
assert.strictEqual(c2.get24Hour().avgPing, 0.5); assert.strictEqual(c2.get24Hour().avgPing, 0.5);
// 1 UP, 1 DOWN // 1 UP, 1 DOWN
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP, 123); await c2.update(UP, 123);
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0.5); assert.strictEqual(uptime, 0.5);
assert.strictEqual(c2.get24Hour().avgPing, 123); assert.strictEqual(c2.get24Hour().avgPing, 123);
// Add 24 hours // Add 24 hours
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP, 0); await c2.update(UP, 0);
await c2.update(UP, 0); await c2.update(UP, 0);
await c2.update(UP, 0); await c2.update(UP, 0);
await c2.update(UP, 1); await c2.update(UP, 1);
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0.8); assert.strictEqual(uptime, 0.8);
assert.strictEqual(c2.get24Hour().avgPing, 0.25); assert.strictEqual(c2.get24Hour().avgPing, 0.25);
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour"); UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour");
// After 24 hours, even if there is no data, the uptime should be still 80% // After 24 hours, even if there is no data, the uptime should be still 80%
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0.8); assert.strictEqual(uptime, 0.8);
assert.strictEqual(c2.get24Hour().avgPing, 0.25); assert.strictEqual(c2.get24Hour().avgPing, 0.25);
// Add more 24 hours (48 hours) // Add more 24 hours (48 hours)
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour"); UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour");
// After 48 hours, even if there is no data, the uptime should be still 80% // After 48 hours, even if there is no data, the uptime should be still 80%
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0.8); assert.strictEqual(uptime, 0.8);
assert.strictEqual(c2.get24Hour().avgPing, 0.25); assert.strictEqual(c2.get24Hour().avgPing, 0.25);
}); });
test("Test get7DayUptime", async (t) => { test("get7Day() calculates 7-day uptime correctly", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
// No data // No data
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let uptime = c2.get7Day().uptime; let uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
// 1 Up // 1 Up
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP); await c2.update(UP);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 1); assert.strictEqual(uptime, 1);
// 2 Up // 2 Up
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP); await c2.update(UP);
await c2.update(UP); await c2.update(UP);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 1); assert.strictEqual(uptime, 1);
// 3 Up // 3 Up
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP); await c2.update(UP);
await c2.update(UP); await c2.update(UP);
await c2.update(UP); await c2.update(UP);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 1); assert.strictEqual(uptime, 1);
// 1 MAINTENANCE // 1 MAINTENANCE
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(MAINTENANCE); await c2.update(MAINTENANCE);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
// 1 PENDING // 1 PENDING
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(PENDING); await c2.update(PENDING);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
// 1 DOWN // 1 DOWN
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
// 2 DOWN // 2 DOWN
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(DOWN); await c2.update(DOWN);
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
// 1 DOWN, 1 UP // 1 DOWN, 1 UP
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(DOWN); await c2.update(DOWN);
await c2.update(UP); await c2.update(UP);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0.5); assert.strictEqual(uptime, 0.5);
// 1 UP, 1 DOWN // 1 UP, 1 DOWN
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP); await c2.update(UP);
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0.5); assert.strictEqual(uptime, 0.5);
// Add 7 days // Add 7 days
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP); await c2.update(UP);
await c2.update(UP); await c2.update(UP);
await c2.update(UP); await c2.update(UP);
await c2.update(UP); await c2.update(UP);
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0.8); assert.strictEqual(uptime, 0.8);
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(7, "day"); UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(7, "day");
// After 7 days, even if there is no data, the uptime should be still 80% // After 7 days, even if there is no data, the uptime should be still 80%
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0.8); assert.strictEqual(uptime, 0.8);
});
}); test("get30Day() calculates 30-day uptime correctly with 1 check per day", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
test("Test get30DayUptime (1 check per day)", async (t) => { let c2 = new UptimeCalculator();
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); let uptime = c2.get30Day().uptime;
assert.strictEqual(uptime, 0);
let c2 = new UptimeCalculator(); let up = 0;
let uptime = c2.get30Day().uptime; let down = 0;
assert.strictEqual(uptime, 0); let flip = true;
for (let i = 0; i < 30; i++) {
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day");
let up = 0; if (flip) {
let down = 0; await c2.update(UP);
let flip = true; up++;
for (let i = 0; i < 30; i++) { } else {
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day"); await c2.update(DOWN);
down++;
}
if (flip) { uptime = c2.get30Day().uptime;
await c2.update(UP); assert.strictEqual(uptime, up / (up + down));
up++;
} else { flip = !flip;
await c2.update(DOWN);
down++;
} }
uptime = c2.get30Day().uptime; // Last 7 days
assert.strictEqual(uptime, up / (up + down)); // Down, Up, Down, Up, Down, Up, Down
// So 3 UP
assert.strictEqual(c2.get7Day().uptime, 3 / 7);
});
flip = !flip; test("get1Year() calculates 1-year uptime correctly with 1 check per day", async () => {
} UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
// Last 7 days let c2 = new UptimeCalculator();
// Down, Up, Down, Up, Down, Up, Down let uptime = c2.get1Year().uptime;
// So 3 UP assert.strictEqual(uptime, 0);
assert.strictEqual(c2.get7Day().uptime, 3 / 7);
});
test("Test get1YearUptime (1 check per day)", async (t) => { let flip = true;
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); for (let i = 0; i < 365; i++) {
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day");
let c2 = new UptimeCalculator(); if (flip) {
let uptime = c2.get1Year().uptime; await c2.update(UP);
assert.strictEqual(uptime, 0); } else {
await c2.update(DOWN);
}
let flip = true; uptime = c2.get30Day().time;
for (let i = 0; i < 365; i++) { flip = !flip;
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day");
if (flip) {
await c2.update(UP);
} else {
await c2.update(DOWN);
} }
uptime = c2.get30Day().time; assert.strictEqual(c2.get1Year().uptime, 183 / 365);
flip = !flip; assert.strictEqual(c2.get30Day().uptime, 15 / 30);
} assert.strictEqual(c2.get7Day().uptime, 4 / 7);
});
assert.strictEqual(c2.get1Year().uptime, 183 / 365); describe("Worst case scenario", () => {
assert.strictEqual(c2.get30Day().uptime, 15 / 30); test("handles year-long simulation with various statuses", {
assert.strictEqual(c2.get7Day().uptime, 4 / 7); skip: process.env.GITHUB_ACTIONS // Not stable on GitHub Actions"
}, async (t) => {
console.log("Memory usage before preparation", memoryUsage());
let c = new UptimeCalculator();
let up = 0;
let down = 0;
let interval = 20;
await t.test("Prepare data", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
// Since 2023-08-12 will be out of 365 range, it starts from 2023-08-13 actually
let actualStartDate = dayjs.utc("2023-08-13 00:00:00").unix();
// Simulate 1s interval for a year
for (let i = 0; i < 365 * 24 * 60 * 60; i += interval) {
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(interval, "second");
//Randomly UP, DOWN, MAINTENANCE, PENDING
let rand = Math.random();
if (rand < 0.25) {
c.update(UP);
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
up++;
}
} else if (rand < 0.5) {
c.update(DOWN);
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
down++;
}
} else if (rand < 0.75) {
c.update(MAINTENANCE);
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
//up++;
}
} else {
c.update(PENDING);
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
down++;
}
}
}
console.log("Final Date: ", UptimeCalculator.currentDate.format("YYYY-MM-DD HH:mm:ss"));
console.log("Memory usage before preparation", memoryUsage());
assert.strictEqual(c.minutelyUptimeDataList.length(), 1440);
assert.strictEqual(c.dailyUptimeDataList.length(), 365);
});
await t.test("get1YearUptime()", async () => {
assert.strictEqual(c.get1Year().uptime, up / (up + down));
});
});
});
}); });
/** /**
@@ -362,64 +421,3 @@ function memoryUsage() {
external: `${formatMemoryUsage(memoryData.external)} -> V8 external memory`, external: `${formatMemoryUsage(memoryData.external)} -> V8 external memory`,
}; };
} }
test("Worst case", async (t) => {
// Disable on GitHub Actions, as it is not stable on it
if (process.env.GITHUB_ACTIONS) {
return;
}
console.log("Memory usage before preparation", memoryUsage());
let c = new UptimeCalculator();
let up = 0;
let down = 0;
let interval = 20;
await t.test("Prepare data", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
// Since 2023-08-12 will be out of 365 range, it starts from 2023-08-13 actually
let actualStartDate = dayjs.utc("2023-08-13 00:00:00").unix();
// Simulate 1s interval for a year
for (let i = 0; i < 365 * 24 * 60 * 60; i += interval) {
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(interval, "second");
//Randomly UP, DOWN, MAINTENANCE, PENDING
let rand = Math.random();
if (rand < 0.25) {
c.update(UP);
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
up++;
}
} else if (rand < 0.5) {
c.update(DOWN);
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
down++;
}
} else if (rand < 0.75) {
c.update(MAINTENANCE);
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
//up++;
}
} else {
c.update(PENDING);
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
down++;
}
}
}
console.log("Final Date: ", UptimeCalculator.currentDate.format("YYYY-MM-DD HH:mm:ss"));
console.log("Memory usage before preparation", memoryUsage());
assert.strictEqual(c.minutelyUptimeDataList.length(), 1440);
assert.strictEqual(c.dailyUptimeDataList.length(), 365);
});
await t.test("get1YearUptime()", async () => {
assert.strictEqual(c.get1Year().uptime, up / (up + down));
});
});
+40 -12
View File
@@ -1,18 +1,46 @@
const test = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const dayjs = require("dayjs");
const { getDaysRemaining, getDaysBetween } = require("../../server/util-server"); const { getDaysRemaining, getDaysBetween } = require("../../server/util-server");
const { SQL_DATETIME_FORMAT } = require("../../src/util");
test("Test getDaysBetween", async (t) => { dayjs.extend(require("dayjs/plugin/utc"));
let days = getDaysBetween(new Date(2025, 9, 7), new Date(2025, 9, 10)); dayjs.extend(require("dayjs/plugin/customParseFormat"));
assert.strictEqual(days, 3);
days = getDaysBetween(new Date(2024, 9, 7), new Date(2025, 9, 10));
assert.strictEqual(days, 368);
});
test("Test getDaysRemaining", async (t) => { describe("Server Utilities", () => {
let days = getDaysRemaining(new Date(2025, 9, 7), new Date(2025, 9, 10)); test("getDaysBetween() calculates days between dates within same month", () => {
assert.strictEqual(days, 3); const days = getDaysBetween(new Date(2025, 9, 7), new Date(2025, 9, 10));
days = getDaysRemaining(new Date(2025, 9, 10), new Date(2025, 9, 7)); assert.strictEqual(days, 3);
assert.strictEqual(days, -3); });
test("getDaysBetween() calculates days between dates across years", () => {
const days = getDaysBetween(new Date(2024, 9, 7), new Date(2025, 9, 10));
assert.strictEqual(days, 368);
});
test("getDaysRemaining() returns positive value when target date is in future", () => {
const days = getDaysRemaining(new Date(2025, 9, 7), new Date(2025, 9, 10));
assert.strictEqual(days, 3);
});
test("getDaysRemaining() returns negative value when target date is in past", () => {
const days = getDaysRemaining(new Date(2025, 9, 10), new Date(2025, 9, 7));
assert.strictEqual(days, -3);
});
test("SQL_DATETIME_FORMAT constant matches MariaDB/MySQL format", () => {
assert.strictEqual(SQL_DATETIME_FORMAT, "YYYY-MM-DD HH:mm:ss");
});
test("SQL_DATETIME_FORMAT produces valid SQL datetime string", () => {
const current = dayjs.utc("2025-12-19T01:04:02.129Z");
const sqlFormat = current.utc().format(SQL_DATETIME_FORMAT);
assert.strictEqual(sqlFormat, "2025-12-19 01:04:02");
// Verify it can be parsed back
const parsedDate = dayjs.utc(sqlFormat, SQL_DATETIME_FORMAT);
assert.strictEqual(parsedDate.unix(), current.unix());
});
}); });
-175
View File
@@ -1,175 +0,0 @@
const { WebSocketServer } = require("ws");
const { describe, test } = require("node:test");
const assert = require("node:assert");
const { WebSocketMonitorType } = require("../../server/monitor-types/websocket-upgrade");
const { UP, PENDING } = require("../../src/util");
describe("Websocket Test", {
}, () => {
test("Non Websocket Server", {}, async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://example.org",
wsIgnoreSecWebsocketAcceptHeader: false,
};
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new Error("Unexpected server response: 200")
);
});
test("Secure Websocket", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://echo.websocket.org",
wsIgnoreSecWebsocketAcceptHeader: false,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "101 - OK",
status: UP,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
test("Insecure Websocket", async (t) => {
t.after(() => wss.close());
const websocketMonitor = new WebSocketMonitorType();
const wss = new WebSocketServer({ port: 8080 });
const monitor = {
url: "ws://localhost:8080",
wsIgnoreSecWebsocketAcceptHeader: false,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "101 - OK",
status: UP,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
test("Non compliant WS server without IgnoreSecWebsocket", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://c.img-cdn.net/yE4s7KehTFyj/",
wsIgnoreSecWebsocketAcceptHeader: false,
};
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new Error("Invalid Sec-WebSocket-Accept header")
);
});
test("Non compliant WS server with IgnoreSecWebsocket", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://c.img-cdn.net/yE4s7KehTFyj/",
wsIgnoreSecWebsocketAcceptHeader: true,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "101 - OK",
status: UP,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
test("Compliant WS server with IgnoreSecWebsocket", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://echo.websocket.org",
wsIgnoreSecWebsocketAcceptHeader: true,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "101 - OK",
status: UP,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
test("Non WS server with IgnoreSecWebsocket", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://example.org",
wsIgnoreSecWebsocketAcceptHeader: true,
};
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new Error("Unexpected server response: 200")
);
});
test("Secure Websocket with Subprotocol", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://echo.websocket.org",
wsIgnoreSecWebsocketAcceptHeader: false,
wsSubprotocol: "ocpp1.6",
};
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new Error("Server sent no subprotocol")
);
});
});