Merge branch 'master' into cancel-in-progress
This commit is contained in:
commit
7fc2a9dea4
48
.github/workflows/build-docker-base.yml
vendored
Normal file
48
.github/workflows/build-docker-base.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
name: Build Docker Base Images
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build-docker-base:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with: { persist-credentials: false }
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ secrets.GHCR_USERNAME }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Build and push base2-slim image
|
||||
run: npm run build-docker-base-slim
|
||||
|
||||
- name: Build and push base2 image
|
||||
run: npm run build-docker-base
|
||||
@ -1,56 +1,65 @@
|
||||
name: Mark PR as draft when changes are requested
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
# pull_request_target is safe here because:
|
||||
# 1. Does not use any external actions; only uses the GitHub CLI via run commands
|
||||
# 2. Has minimal permissions
|
||||
# 3. Doesn't checkout or execute any untrusted code from PRs
|
||||
# 4. Only adds/removes labels or changes the draft status
|
||||
on: # zizmor: ignore[dangerous-triggers]
|
||||
pull_request_target:
|
||||
types:
|
||||
- review_submitted
|
||||
- labeled
|
||||
- ready_for_review
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
mark-draft:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
if: |
|
||||
(
|
||||
github.event_name == 'pull_request_review' &&
|
||||
github.event.action == 'review_submitted' &&
|
||||
github.event.review.state == 'changes_requested'
|
||||
) || (
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.action == 'labeled' &&
|
||||
github.event.label.name == 'pr:please address review comments'
|
||||
)
|
||||
steps:
|
||||
- name: Add label on requested changes
|
||||
if: github.event_name == 'pull_request_review'
|
||||
if: github.event.review.state == 'changes_requested'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.MARK_AS_DRAFT_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh issue edit "$PR_NUMBER" \
|
||||
--repo "$REPO" \
|
||||
gh issue edit "${{ github.event.pull_request.number }}" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--add-label "pr:please address review comments"
|
||||
|
||||
- name: Mark PR as draft
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.MARK_AS_DRAFT_TOKEN }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
run: gh pr ready "$PR_URL" --undo || true
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh pr ready "${{ github.event.pull_request.number }}" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--undo || true
|
||||
# || true to ignore the case where the pr is already a draft
|
||||
|
||||
ready-for-review:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' && github.event.action == 'ready_for_review'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
if: github.event.action == 'ready_for_review'
|
||||
steps:
|
||||
- name: Update labels for review
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.MARK_AS_DRAFT_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh issue edit "$PR_NUMBER" \
|
||||
--repo "$REPO" \
|
||||
gh issue edit "${{ github.event.pull_request.number }}" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--remove-label "pr:please address review comments" || true
|
||||
|
||||
gh issue edit "$PR_NUMBER" \
|
||||
--repo "$REPO" \
|
||||
gh issue edit "${{ github.event.pull_request.number }}" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--add-label "pr:needs review"
|
||||
|
||||
@ -5,7 +5,7 @@ const { escape } = require("html-escaper");
|
||||
* Returns a string that represents the javascript that is required to insert the Plausible Analytics script
|
||||
* into a webpage.
|
||||
* @param {string} scriptUrl the Plausible Analytics script url.
|
||||
* @param {string} domainsToMonitor Domains to track seperated by a ',' to add Plausible Analytics script.
|
||||
* @param {string} domainsToMonitor Domains to track separated by a ',' to add Plausible Analytics script.
|
||||
* @returns {string} HTML script tags to inject into page
|
||||
*/
|
||||
function getPlausibleAnalyticsScript(scriptUrl, domainsToMonitor) {
|
||||
|
||||
@ -159,20 +159,19 @@ class DomainExpiry extends BeanModel {
|
||||
const tld = parseTld(target);
|
||||
|
||||
// Avoid logging for incomplete/invalid input while editing monitors.
|
||||
if (!tld.domain) {
|
||||
throw new TranslatableError("domain_expiry_unsupported_invalid_domain", { hostname: tld.hostname });
|
||||
}
|
||||
if (!tld.publicSuffix) {
|
||||
throw new TranslatableError("domain_expiry_unsupported_public_suffix", { publicSuffix: tld.publicSuffix });
|
||||
}
|
||||
if (tld.isIp) {
|
||||
throw new TranslatableError("domain_expiry_unsupported_is_ip", { hostname: tld.hostname });
|
||||
}
|
||||
|
||||
// No one-letter public suffix exists; treat this as an incomplete/invalid input while typing.
|
||||
if (tld.publicSuffix.length < 2) {
|
||||
throw new TranslatableError("domain_expiry_public_suffix_too_short", { publicSuffix: tld.publicSuffix });
|
||||
}
|
||||
if (!tld.isIcann) {
|
||||
throw new TranslatableError("domain_expiry_unsupported_is_icann", {
|
||||
domain: tld.domain,
|
||||
publicSuffix: tld.publicSuffix,
|
||||
});
|
||||
}
|
||||
|
||||
const rdap = await getRdapServer(tld.publicSuffix);
|
||||
if (!rdap) {
|
||||
|
||||
@ -3,26 +3,61 @@ const { log, UP } = require("../../src/util");
|
||||
const dayjs = require("dayjs");
|
||||
const postgresConParse = require("pg-connection-string").parse;
|
||||
const { Client } = require("pg");
|
||||
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 PostgresMonitorType extends MonitorType {
|
||||
name = "postgres";
|
||||
|
||||
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";
|
||||
}
|
||||
await this.postgresQuery(monitor.databaseConnectionString, query);
|
||||
|
||||
heartbeat.msg = "";
|
||||
heartbeat.status = UP;
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
const conditions = monitor.conditions ? ConditionExpressionGroup.fromMonitor(monitor) : null;
|
||||
const hasConditions = conditions && conditions.children && conditions.children.length > 0;
|
||||
|
||||
const startTime = dayjs().valueOf();
|
||||
|
||||
try {
|
||||
if (hasConditions) {
|
||||
// When conditions are enabled, expect a single value result
|
||||
const result = await this.postgresQuerySingleValue(monitor.databaseConnectionString, query);
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
const conditionsResult = evaluateExpressionGroup(conditions, { result: String(result) });
|
||||
|
||||
if (!conditionsResult) {
|
||||
throw new Error(`Query result did not meet the specified conditions (${result})`);
|
||||
}
|
||||
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = "Query did meet specified conditions";
|
||||
} else {
|
||||
// Backwards compatible: just check connection and return row count
|
||||
const result = await this.postgresQuery(monitor.databaseConnectionString, query);
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = result;
|
||||
}
|
||||
} catch (error) {
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
// Re-throw condition errors as-is, wrap database errors
|
||||
if (error.message.includes("did not meet the specified conditions")) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Database connection/query failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,6 +111,75 @@ class PostgresMonitorType extends MonitorType {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a query on Postgres
|
||||
* @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
|
||||
*/
|
||||
async postgresQuerySingleValue(connectionString, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const config = postgresConParse(connectionString);
|
||||
|
||||
// Fix #3868, which true/false is not parsed to boolean
|
||||
if (typeof config.ssl === "string") {
|
||||
config.ssl = config.ssl === "true";
|
||||
}
|
||||
|
||||
if (config.password === "") {
|
||||
// See https://github.com/brianc/node-postgres/issues/1927
|
||||
reject(new Error("Password is undefined."));
|
||||
return;
|
||||
}
|
||||
const client = new Client(config);
|
||||
|
||||
client.on("error", (error) => {
|
||||
log.debug(this.name, "Error caught in the error event handler.");
|
||||
reject(error);
|
||||
});
|
||||
|
||||
client.connect((err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
client.end();
|
||||
} else {
|
||||
// Connected here
|
||||
try {
|
||||
client.query(query, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
// Check if we have results
|
||||
if (!res.rows || res.rows.length === 0) {
|
||||
reject(new Error("Query returned no results"));
|
||||
return;
|
||||
}
|
||||
// Check if we have multiple rows
|
||||
if (res.rows.length > 1) {
|
||||
reject(new Error("Multiple values were found, expected only one value"));
|
||||
return;
|
||||
}
|
||||
const firstRow = res.rows[0];
|
||||
const columnNames = Object.keys(firstRow);
|
||||
// Check if we have multiple columns
|
||||
if (columnNames.length > 1) {
|
||||
reject(new Error("Multiple columns were found, expected only one value"));
|
||||
return;
|
||||
}
|
||||
resolve(firstRow[columnNames[0]]);
|
||||
}
|
||||
client.end();
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
client.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="shadow-box mb-3" :style="boxStyle">
|
||||
<div class="shadow-box mb-3 p-0" :style="boxStyle">
|
||||
<div class="list-header">
|
||||
<div class="header-top">
|
||||
<div class="select-checkbox-wrapper">
|
||||
@ -28,7 +28,7 @@
|
||||
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
|
||||
<font-awesome-icon icon="times" />
|
||||
</a>
|
||||
<form>
|
||||
<form @submit.prevent>
|
||||
<input
|
||||
v-model="searchText"
|
||||
class="form-control search-input"
|
||||
@ -89,7 +89,7 @@
|
||||
</div>
|
||||
<div
|
||||
ref="monitorList"
|
||||
class="monitor-list"
|
||||
class="monitor-list px-2"
|
||||
:class="{ scrollbar: scrollbar }"
|
||||
:style="monitorListStyle"
|
||||
data-testid="monitor-list"
|
||||
@ -536,7 +536,6 @@ export default {
|
||||
.list-header {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-radius: 10px 10px 0 0;
|
||||
margin: -10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
|
||||
@ -679,7 +678,6 @@ export default {
|
||||
|
||||
@media (max-width: 770px) {
|
||||
.list-header {
|
||||
margin: -20px;
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
@ -24,12 +24,12 @@
|
||||
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ disabled: !monitor.active }">
|
||||
<div class="row">
|
||||
<div
|
||||
class="col-9 col-xl-6 small-padding"
|
||||
class="col-9 col-xl-6 small-padding d-flex align-items-center"
|
||||
:class="{
|
||||
'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none',
|
||||
}"
|
||||
>
|
||||
<div class="info">
|
||||
<div class="info d-flex align-items-center gap-2">
|
||||
<Uptime :monitor="monitor" type="24" :pill="true" />
|
||||
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed">
|
||||
<font-awesome-icon
|
||||
@ -383,6 +383,7 @@ export default {
|
||||
|
||||
/* We don't want the padding change due to the border animated */
|
||||
.item {
|
||||
padding: 12px 15px;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
"General Monitor Type": "General Monitor Type",
|
||||
"Passive Monitor Type": "Passive Monitor Type",
|
||||
"Specific Monitor Type": "Specific Monitor Type",
|
||||
"markdownSupported": "Markdown syntax supported",
|
||||
"markdownSupported": "Markdown syntax supported. If using HTML, avoid leading spaces to prevent formatting issues.",
|
||||
"pauseDashboardHome": "Pause",
|
||||
"Pause": "Pause",
|
||||
"Name": "Name",
|
||||
@ -1297,9 +1297,8 @@
|
||||
"domainExpiryDescription": "Trigger notification when domain names expires in:",
|
||||
"domain_expiry_unsupported_monitor_type": "Domain expiry monitoring is not supported for this monitor type",
|
||||
"domain_expiry_unsupported_missing_target": "No valid domain or hostname is configured for this monitor",
|
||||
"domain_expiry_unsupported_invalid_domain": "The configured value \"{hostname}\" is not a valid domain name",
|
||||
"domain_expiry_public_suffix_too_short": "\".{publicSuffix}\" is too short for a top level domain",
|
||||
"domain_expiry_unsupported_public_suffix": "The domain \"{domain}\" does not have a valid public suffix",
|
||||
"domain_expiry_unsupported_is_icann": "The domain \"{domain}\" is not a candidate for domain expiry monitoring, because its public suffix \".{publicSuffix}\" is not ICAN",
|
||||
"domain_expiry_unsupported_is_ip": "\"{hostname}\" is an IP address. Domain expiry monitoring requires a domain name",
|
||||
"domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint": "Domain expiry monitoring is not available for \".{publicSuffix}\" because no RDAP service is listed by IANA",
|
||||
"minimumIntervalWarning": "Intervals below 20 seconds may result in poor performance.",
|
||||
|
||||
@ -2755,7 +2755,7 @@ message HealthCheckResponse {
|
||||
this.monitor.jsonPath = "$";
|
||||
}
|
||||
|
||||
// Set default condition for for jsonPathOperator
|
||||
// Set default condition for jsonPathOperator
|
||||
if (!this.monitor.jsonPathOperator) {
|
||||
this.monitor.jsonPathOperator = "==";
|
||||
}
|
||||
|
||||
@ -25,9 +25,7 @@
|
||||
class="form-control"
|
||||
data-testid="description-input"
|
||||
></textarea>
|
||||
<div class="form-text">
|
||||
{{ $t("markdownSupported") }}
|
||||
</div>
|
||||
<div class="form-text">{{ $t("markdownSupported") }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Text -->
|
||||
@ -39,9 +37,7 @@
|
||||
class="form-control"
|
||||
data-testid="footer-text-input"
|
||||
></textarea>
|
||||
<div class="form-text">
|
||||
{{ $t("markdownSupported") }}
|
||||
</div>
|
||||
<div class="form-text">{{ $t("markdownSupported") }}</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
|
||||
@ -49,5 +49,203 @@ describe(
|
||||
|
||||
await assert.rejects(postgresMonitor.check(monitor, heartbeat, {}), regex);
|
||||
});
|
||||
|
||||
test("check() sets status to UP when custom query returns single value", async () => {
|
||||
// The default timeout of 30 seconds might not be enough for the container to start
|
||||
const postgresContainer = await new PostgreSqlContainer("postgres:latest")
|
||||
.withStartupTimeout(60000)
|
||||
.start();
|
||||
|
||||
const postgresMonitor = new PostgresMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString: postgresContainer.getConnectionUri(),
|
||||
databaseQuery: "SELECT 42",
|
||||
conditions: "[]",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
try {
|
||||
await postgresMonitor.check(monitor, heartbeat, {});
|
||||
assert.strictEqual(heartbeat.status, UP, `Expected status ${UP} but got ${heartbeat.status}`);
|
||||
} finally {
|
||||
await postgresContainer.stop();
|
||||
}
|
||||
});
|
||||
test("check() sets status to UP when custom query result meets condition", async () => {
|
||||
const postgresContainer = await new PostgreSqlContainer("postgres:latest")
|
||||
.withStartupTimeout(60000)
|
||||
.start();
|
||||
|
||||
const postgresMonitor = new PostgresMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString: postgresContainer.getConnectionUri(),
|
||||
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 postgresMonitor.check(monitor, heartbeat, {});
|
||||
assert.strictEqual(heartbeat.status, UP, `Expected status ${UP} but got ${heartbeat.status}`);
|
||||
} finally {
|
||||
await postgresContainer.stop();
|
||||
}
|
||||
});
|
||||
test("check() rejects when custom query result does not meet condition", async () => {
|
||||
const postgresContainer = await new PostgreSqlContainer("postgres:latest")
|
||||
.withStartupTimeout(60000)
|
||||
.start();
|
||||
|
||||
const postgresMonitor = new PostgresMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString: postgresContainer.getConnectionUri(),
|
||||
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(
|
||||
postgresMonitor.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 postgresContainer.stop();
|
||||
}
|
||||
});
|
||||
test("check() rejects when query returns no results with conditions", async () => {
|
||||
const postgresContainer = await new PostgreSqlContainer("postgres:latest")
|
||||
.withStartupTimeout(60000)
|
||||
.start();
|
||||
|
||||
const postgresMonitor = new PostgresMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString: postgresContainer.getConnectionUri(),
|
||||
databaseQuery: "SELECT 1 WHERE 1 = 0",
|
||||
conditions: JSON.stringify([
|
||||
{
|
||||
type: "expression",
|
||||
andOr: "and",
|
||||
variable: "result",
|
||||
operator: "equals",
|
||||
value: "1",
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
try {
|
||||
await assert.rejects(
|
||||
postgresMonitor.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 postgresContainer.stop();
|
||||
}
|
||||
});
|
||||
test("check() rejects when query returns multiple rows with conditions", async () => {
|
||||
const postgresContainer = await new PostgreSqlContainer("postgres:latest")
|
||||
.withStartupTimeout(60000)
|
||||
.start();
|
||||
|
||||
const postgresMonitor = new PostgresMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString: postgresContainer.getConnectionUri(),
|
||||
databaseQuery: "SELECT 1 UNION ALL SELECT 2",
|
||||
conditions: JSON.stringify([
|
||||
{
|
||||
type: "expression",
|
||||
andOr: "and",
|
||||
variable: "result",
|
||||
operator: "equals",
|
||||
value: "1",
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
try {
|
||||
await assert.rejects(
|
||||
postgresMonitor.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 postgresContainer.stop();
|
||||
}
|
||||
});
|
||||
test("check() rejects when query returns multiple columns with conditions", async () => {
|
||||
const postgresContainer = await new PostgreSqlContainer("postgres:latest")
|
||||
.withStartupTimeout(60000)
|
||||
.start();
|
||||
|
||||
const postgresMonitor = new PostgresMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString: postgresContainer.getConnectionUri(),
|
||||
databaseQuery: "SELECT 1 AS col1, 2 AS col2",
|
||||
conditions: JSON.stringify([
|
||||
{
|
||||
type: "expression",
|
||||
andOr: "and",
|
||||
variable: "result",
|
||||
operator: "equals",
|
||||
value: "1",
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
try {
|
||||
await assert.rejects(
|
||||
postgresMonitor.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 postgresContainer.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@ -96,58 +96,26 @@ describe("Domain Expiry", () => {
|
||||
});
|
||||
|
||||
describe("Domain Parsing", () => {
|
||||
test("throws error for invalid domain (no domain part)", async () => {
|
||||
test("throws error for IP address (isIp check)", async () => {
|
||||
const monitor = {
|
||||
type: "http",
|
||||
url: "https://",
|
||||
url: "https://127.0.0.1",
|
||||
domainExpiryNotification: true,
|
||||
};
|
||||
await assert.rejects(
|
||||
async () => await DomainExpiry.checkSupport(monitor),
|
||||
(error) => {
|
||||
assert.strictEqual(error.constructor.name, "TranslatableError");
|
||||
assert.strictEqual(error.message, "domain_expiry_unsupported_invalid_domain");
|
||||
assert.strictEqual(error.message, "domain_expiry_unsupported_is_ip");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("throws error for IPv4 address instead of domain", async () => {
|
||||
test("throws error for too short suffix(example.a)", async () => {
|
||||
const monitor = {
|
||||
type: "http",
|
||||
url: "https://192.168.1.1",
|
||||
domainExpiryNotification: true,
|
||||
};
|
||||
await assert.rejects(
|
||||
async () => await DomainExpiry.checkSupport(monitor),
|
||||
(error) => {
|
||||
assert.strictEqual(error.constructor.name, "TranslatableError");
|
||||
assert.strictEqual(error.message, "domain_expiry_unsupported_invalid_domain");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("throws error for IPv6 address", async () => {
|
||||
const monitor = {
|
||||
type: "http",
|
||||
url: "https://[2001:db8::1]",
|
||||
domainExpiryNotification: true,
|
||||
};
|
||||
await assert.rejects(
|
||||
async () => await DomainExpiry.checkSupport(monitor),
|
||||
(error) => {
|
||||
assert.strictEqual(error.constructor.name, "TranslatableError");
|
||||
assert.strictEqual(error.message, "domain_expiry_unsupported_invalid_domain");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("throws error for single-letter TLD", async () => {
|
||||
const monitor = {
|
||||
type: "http",
|
||||
url: "https://example.x",
|
||||
url: "https://example.a",
|
||||
domainExpiryNotification: true,
|
||||
};
|
||||
await assert.rejects(
|
||||
@ -159,6 +127,22 @@ describe("Domain Expiry", () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("throws error for non-ICANN TLD (e.g. .local)", async () => {
|
||||
const monitor = {
|
||||
type: "http",
|
||||
url: "https://example.local",
|
||||
domainExpiryNotification: true,
|
||||
};
|
||||
await assert.rejects(
|
||||
async () => await DomainExpiry.checkSupport(monitor),
|
||||
(error) => {
|
||||
assert.strictEqual(error.constructor.name, "TranslatableError");
|
||||
assert.strictEqual(error.message, "domain_expiry_unsupported_is_icann");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases & RDAP Support", () => {
|
||||
@ -205,22 +189,6 @@ describe("Domain Expiry", () => {
|
||||
assert.strictEqual(supportInfo.domain, "example.com");
|
||||
assert.strictEqual(supportInfo.tld, "com");
|
||||
});
|
||||
|
||||
test("throws error for unsupported TLD without RDAP endpoint", async () => {
|
||||
const monitor = {
|
||||
type: "http",
|
||||
url: "https://example.localhost",
|
||||
domainExpiryNotification: true,
|
||||
};
|
||||
await assert.rejects(
|
||||
async () => await DomainExpiry.checkSupport(monitor),
|
||||
(error) => {
|
||||
assert.strictEqual(error.constructor.name, "TranslatableError");
|
||||
assert.strictEqual(error.message, "domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user