Compare commits

...

13 Commits

Author SHA1 Message Date
Louis Lam
60460442f8 Update to 1.18.3 2022-10-07 00:25:34 +08:00
Louis Lam
959ecc65ff Merge remote-tracking branch 'origin/master' 2022-10-06 23:28:29 +08:00
Louis Lam
c24b64921d Fix #2183 ntfy issue 2022-10-06 23:28:06 +08:00
Ben Scobie
c28d8ddff9 Correctly handle multiple IPs in X-Forwarded-For (#2177)
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2022-10-05 23:45:21 +08:00
CL0Pinette
528a615fb2 Add free.fr SMS notification provider (#2159) 2022-10-05 17:30:49 +08:00
Louis Lam
b993859926 Drop Jest e2e testing (#2174) 2022-10-05 14:26:30 +08:00
Louis Lam
a5c102e750 Update README.md 2022-10-05 14:19:50 +08:00
Cyril59310
64ba2dce24 Update FR language (#2173) 2022-10-05 13:57:38 +08:00
Louis Lam
e5145a209a Update cypress config 2022-10-05 13:29:03 +08:00
Louis Lam
12696dd53e Update README.md 2022-10-05 12:54:25 +08:00
Muhammed Hussein karimi
d565320f74 New Demo Server (#2172)
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2022-10-05 12:53:58 +08:00
Sympatron GmbH
f1a9046193 Prevent terminal window from showing when using ping on Windows (#2152) 2022-10-04 23:30:19 +08:00
Louis Lam
afbc283423 Move Cypress directory and convert it to JavaScript (#2170) 2022-10-04 22:23:57 +08:00
37 changed files with 324 additions and 1918 deletions

View File

@@ -15,11 +15,10 @@ It is a self-hosted monitoring tool like "Uptime Robot".
Try it!
https://demo.uptime.kuma.pet
- Tokyo Demo Server: https://demo.uptime.kuma.pet (Sponsored by [Uptime Kuma Sponsors](https://github.com/louislam/uptime-kuma#%EF%B8%8F-sponsors))
- Europe Demo Server: https://demo.uptime-kuma.karimi.dev:27000 (Provided by [@mhkarimi1383](https://github.com/mhkarimi1383))
It is a temporary live demo, all data will be deleted after 10 minutes. The server is located in Tokyo, so if you live far from there, it may affect your experience. I suggest that you should install and try it out for the best demo experience.
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
It is a temporary live demo, all data will be deleted after 10 minutes. Use the one that is closer to you, but I suggest that you should install and try it out for the best demo experience.
## ⭐ Features

28
config/cypress.config.js Normal file
View File

@@ -0,0 +1,28 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
projectId: "vyjuem",
e2e: {
experimentalStudio: true,
setupNodeEvents(on, config) {
},
fixturesFolder: "test/cypress/fixtures",
screenshotsFolder: "test/cypress/screenshots",
videosFolder: "test/cypress/videos",
downloadsFolder: "test/cypress/downloads",
supportFile: "test/cypress/support/e2e.js",
baseUrl: "http://localhost:3002",
defaultCommandTimeout: 10000,
pageLoadTimeout: 60000,
viewportWidth: 1920,
viewportHeight: 1080,
specPattern: [
"test/cypress/e2e/setup.cy.js",
"test/cypress/e2e/**/*.js"
],
},
env: {
baseUrl: "http://localhost:3002",
},
});

View File

@@ -1,33 +0,0 @@
const PuppeteerEnvironment = require("jest-environment-puppeteer");
const util = require("util");
class DebugEnv extends PuppeteerEnvironment {
async handleTestEvent(event, state) {
const ignoredEvents = [
"setup",
"add_hook",
"start_describe_definition",
"add_test",
"finish_describe_definition",
"run_start",
"run_describe_start",
"test_start",
"hook_start",
"hook_success",
"test_fn_start",
"test_fn_success",
"test_done",
"run_describe_finish",
"run_finish",
"teardown",
"test_fn_failure",
];
if (!ignoredEvents.includes(event.name)) {
console.log(
new Date().toString() + ` Unhandled event [${event.name}] ` + util.inspect(event)
);
}
}
}
module.exports = DebugEnv;

View File

@@ -1,5 +0,0 @@
module.exports = {
"rootDir": "..",
"testRegex": "./test/frontend.spec.js",
};

View File

@@ -1,20 +0,0 @@
module.exports = {
"launch": {
"dumpio": true,
"slowMo": 500,
"headless": process.env.HEADLESS_TEST || false,
"userDataDir": "./data/test-chrome-profile",
args: [
"--disable-setuid-sandbox",
"--disable-gpu",
"--disable-dev-shm-usage",
"--no-default-browser-check",
"--no-experiments",
"--no-first-run",
"--no-pings",
"--no-sandbox",
"--no-zygote",
"--single-process",
],
}
};

View File

@@ -1,12 +0,0 @@
module.exports = {
"verbose": true,
"preset": "jest-puppeteer",
"globals": {
"__DEV__": true
},
"testRegex": "./test/e2e.spec.js",
"testEnvironment": "./config/jest-debug-env.js",
"rootDir": "..",
"testTimeout": 30000,
};

View File

@@ -1,15 +0,0 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "http://localhost:3002",
defaultCommandTimeout: 10000,
pageLoadTimeout: 60000,
viewportWidth: 1920,
viewportHeight: 1080,
specPattern: ["cypress/e2e/setup.cy.ts", "cypress/e2e/**/*.ts"],
},
env: {
baseUrl: "http://localhost:3002",
},
});

View File

@@ -1,24 +0,0 @@
import { actor } from "../support/actors/actor";
import { DEFAULT_USER_DATA } from "../support/const/user-data";
import { DashboardPage } from "../support/pages/dasboard-page";
import { SetupPage } from "../support/pages/setup-page";
describe("user can create a new account on setup page", () => {
before(() => {
cy.visit("/setup");
});
it("user can create new account", () => {
cy.url().should("be.equal", SetupPage.url);
actor.setupTask.fillAndSubmitSetupForm(
DEFAULT_USER_DATA.username,
DEFAULT_USER_DATA.password,
DEFAULT_USER_DATA.password
);
cy.url().should("be.equal", DashboardPage.url);
cy.get('[role="alert"]')
.should("be.visible")
.and("contain.text", "Added Successfully.");
});
});

View File

@@ -1,8 +0,0 @@
import { SetupTask } from "../tasks/setup-task";
class Actor {
setupTask: SetupTask = new SetupTask();
}
const actor = new Actor();
export { actor };

View File

@@ -1 +0,0 @@
import "./commands";

View File

@@ -1,15 +0,0 @@
import { SetupPage } from "../pages/setup-page";
export class SetupTask {
fillAndSubmitSetupForm(
username: string,
password: string,
passwordRepeat: string
) {
cy.get(SetupPage.usernameInput).type(username);
cy.get(SetupPage.passWordInput).type(password);
cy.get(SetupPage.passwordRepeatInput).type(passwordRepeat);
cy.get(SetupPage.submitSetupForm).click();
}
}

1367
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.18.2",
"version": "1.18.3",
"license": "MIT",
"repository": {
"type": "git",
@@ -23,11 +23,9 @@
"start-server": "node server/server.js",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
"build": "vite build --config ./config/vite.config.js",
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
"test": "node test/prepare-test-server.js && npm run jest-backend",
"test-with-build": "npm run build && npm test",
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend",
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
"jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
"jest-backend": "cross-env TEST_BACKEND=1 jest --runInBand --detectOpenHandles --forceExit --config=./config/jest-backend.config.js",
"tsc": "tsc",
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
@@ -40,7 +38,7 @@
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.18.2 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.18.3 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@@ -62,7 +60,8 @@
"build-dist-and-restart": "npm run build && npm run start-server-dev",
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
"cy:test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --e2e",
"cy:run": "npx cypress run --browser chrome --headless"
"cy:run": "npx cypress run --browser chrome --headless --config-file ./config/cypress.config.js",
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\""
},
"dependencies": {
"@louislam/sqlite3": "~15.0.6",
@@ -139,12 +138,10 @@
"eslint-plugin-vue": "~8.7.1",
"favico.js": "^0.3.10",
"jest": "~27.2.5",
"jest-puppeteer": "~6.0.3",
"postcss-html": "~1.5.0",
"postcss-rtlcss": "~3.7.2",
"postcss-scss": "~4.0.4",
"prismjs": "^1.27.0",
"puppeteer": "~13.1.3",
"qrcode": "~1.5.0",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "~1.42.1",

View File

@@ -0,0 +1,24 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class FreeMobile extends NotificationProvider {
name = "FreeMobile";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, {
"user": notification.freemobileUser,
"pass": notification.freemobilePass,
});
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = FreeMobile;

View File

@@ -9,7 +9,7 @@ class Ntfy extends NotificationProvider {
let okMsg = "Sent Successfully.";
try {
let headers = {};
if (notification.ntfyusername.length > 0) {
if (notification.ntfyusername) {
headers = {
"Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"),
};

View File

@@ -9,6 +9,7 @@ const ClickSendSMS = require("./notification-providers/clicksendsms");
const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
const Feishu = require("./notification-providers/feishu");
const FreeMobile = require("./notification-providers/freemobile");
const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
const Gotify = require("./notification-providers/gotify");
@@ -63,6 +64,7 @@ class Notification {
new DingDing(),
new Discord(),
new Feishu(),
new FreeMobile(),
new GoogleChat(),
new Gorush(),
new Gotify(),

View File

@@ -105,7 +105,7 @@ Ping.prototype.send = function (callback) {
let _exited;
let _errored;
this._ping = spawn(this._bin, this._args); // spawn the binary
this._ping = spawn(this._bin, this._args, { windowsHide: true }); // spawn the binary
this._ping.on("error", function (err) { // handle binary errors
_errored = true;

View File

@@ -138,7 +138,9 @@ class UptimeKumaServer {
}
if (await Settings.get("trustProxy")) {
return socket.client.conn.request.headers["x-forwarded-for"]
const forwardedFor = socket.client.conn.request.headers["x-forwarded-for"];
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|| socket.client.conn.request.headers["x-real-ip"]
|| clientIP.replace(/^.*:/, "");
} else {

View File

@@ -557,7 +557,7 @@ exports.doubleCheckPassword = async (socket, currentPassword) => {
exports.startUnitTest = async () => {
console.log("Starting unit test...");
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
const child = childProcess.spawn(npm, [ "run", "jest" ]);
const child = childProcess.spawn(npm, [ "run", "jest-backend" ]);
child.stdout.on("data", (data) => {
console.log(data.toString());

View File

@@ -0,0 +1,12 @@
<template>
<div class="mb-3">
<label for="freemobileUser" class="form-label">{{ $t("Free Mobile User Identifier") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="freemobileUser" v-model="$parent.notification.freemobileUser" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="freemobilePass" class="form-label">{{ $t("Free Mobile API Key") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="freemobilePass" v-model="$parent.notification.freemobilePass" type="text" class="form-control" required>
</div>
</template>

View File

@@ -7,6 +7,7 @@ import ClickSendSMS from "./ClickSendSMS.vue";
import DingDing from "./DingDing.vue";
import Discord from "./Discord.vue";
import Feishu from "./Feishu.vue";
import FreeMobile from "./FreeMobile.vue";
import GoogleChat from "./GoogleChat.vue";
import Gorush from "./Gorush.vue";
import Gotify from "./Gotify.vue";
@@ -56,6 +57,7 @@ const NotificationFormList = {
"DingDing": DingDing,
"discord": Discord,
"Feishu": Feishu,
"FreeMobile": FreeMobile,
"GoogleChat": GoogleChat,
"gorush": Gorush,
"gotify": Gotify,

View File

@@ -200,7 +200,7 @@ export default {
chatIDNotFound: "ID du salon introuvable, envoyez un message via le bot avant",
webhook: "Webhook",
"Post URL": "Post URL",
"Content Type": "Content Type",
"Content Type": "Type de contenu",
webhookJsonDesc: "{0} est bien/bon pour tous les serveurs HTTP modernes comme express.js",
webhookFormDataDesc: "{multipart} est bien/bon pour du PHP, vous avez juste besoin de mettre le json via/depuis {decodeFunction}",
smtp: "Email (SMTP)",
@@ -227,8 +227,8 @@ export default {
wayToCheckSignalURL: "Vous pouvez regarder l'URL sur comment le mettre en place :",
signalImportant: "IMPORTANT : Vous ne pouvez pas mixer les groupes et les numéros en destinataires !",
gotify: "Gotify",
"Application Token": "Application Token",
"Server URL": "Server URL",
"Application Token": "Jeton d'application",
"Server URL": "URL du serveur",
Priority: "Priorité",
slack: "Slack",
"Icon Emoji": "Icon Emoji",
@@ -287,7 +287,7 @@ export default {
promosmsTypeSpeed: "SMS SPEED - La plus haute des priorités dans le système. Très rapide et fiable mais cher (environ le double du prix d'un SMS FULL).",
promosmsPhoneNumber: "Numéro de téléphone (Poiur les déstinataires Polonais, vous pouvez enlever les codes interna.)",
promosmsSMSSender: "SMS Expéditeur : Nom pré-enregistré ou l'un de base : InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Primary Base URL": "Primary Base URL",
"Primary Base URL": "URL principale",
emailCustomSubject: "Sujet personalisé",
clicksendsms: "ClickSend SMS",
checkPrice: "Vérification {0} tarifs :",
@@ -342,13 +342,13 @@ export default {
Title: "Titre",
Content: "Contenu",
Style: "Style",
info: "info",
info: "Info",
warning: "Attention",
danger: "danger",
danger: "Danger",
error: "Erreur",
critical: "critique",
primary: "primaire",
light: "blanc",
critical: "Critique",
primary: "Primaire",
light: "Blanc",
dark: "Noir",
Post: "Post",
"Please input title and content": "Veuillez entrer le titre et le contenu",
@@ -390,7 +390,7 @@ export default {
Installed: "Installé",
"Not installed": "Pas installé",
"Remove Token": "Supprimer le jeton",
Slug: "chemin",
Slug: "Chemin",
"The slug is already taken. Please choose another slug.": "Le chemin est déjà pris. Veuillez choisir un autre chemin.",
Authentication: "Authentication",
"Page Not Found": "Page non trouvée",
@@ -431,4 +431,104 @@ export default {
"Trigger type:": "Type de déclencheur:",
"Event type:": "Type d'événement:",
"Event data:": "Données d'événement:",
topic: "Topic",
topicExplanation: "MQTT sujet à surveiller",
successMessage: "Message de réussite",
successMessageExplanation: "MQTT message qui sera considéré comme un succès",
"Powered by": "Propulsé par",
serwersms: "SerwerSMS.pl",
stackfield: "Stackfield",
smtpDkimSettings: "Paramètres DKIM",
smtpDkimDesc: "Veuillez vous référer au Nodemailer DKIM {0} pour l'utilisation.",
documentation: "Documentation",
smtpDkimDomain: "Nom de domaine",
smtpDkimKeySelector: "Sélecteur de clé",
smtpDkimPrivateKey: "Clé privée",
smtpDkimHashAlgo: "Algorithme de hachage (facultatif)",
smtpDkimheaderFieldNames: "Clés d'en-tête à signer (facultatif)",
smtpDkimskipFields: "Clés d'en-tête à ne pas signer (facultatif)",
wayToGetPagerDutyKey: "Vous pouvez l'obtenir en allant dans Service -> Annuaire des services -> (Sélectionner un service) -> Intégrations -> Ajouter une intégration. Ici, vous pouvez rechercher \"Events API V2\". Plus d'infos {0}",
"Integration Key": "Clé d'intégration",
"Integration URL": "URL d'intégration",
"Auto resolve or acknowledged": "Résolution automatique ou accusé de réception",
"do nothing": "ne fais rien",
"auto acknowledged": "accusé de réception automatique",
"auto resolve": "résolution automatique",
AccessKeyId: "ID de clé d'accès",
SecretAccessKey: "Clé secrète d'accès",
PhoneNumbers: "Les numéros de téléphone",
SignName: "Signature",
"Sms template must contain parameters: ": "Le modèle de SMS doit contenir des paramètres : ",
SecretKey: "Clé secrète",
"For safety, must use secret key": "Pour la sécurité, doit utiliser la clé secrète",
"Device Token": "Jeton d'appareil",
Platform: "Plateforme",
Retry: "Recommencez",
Topic: "Topic",
"Proxy server has authentication": "Le serveur proxy a une authentification",
Running: "Fonctionne",
"Not running": "Ne fonctionne pas",
Start: "Start",
Stop: "Stop",
"Uptime Kuma": "Uptime Kuma",
"No Proxy": "Pas de Proxy",
"HTTP Basic Auth": "Authentification de base HTTP",
"Reverse Proxy": "Proxy inverse",
wayToGetCloudflaredURL: "(Télécharger cloudflared depuis {0})",
cloudflareWebsite: "le site Cloudflare ",
"Message:": "Message:",
"Don't know how to get the token? Please read the guide:": "Vous ne savez pas comment obtenir le jeton ? Veuillez lire le guide:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "La connexion actuelle peut être perdue si vous vous connectez actuellement via Cloudflare Tunnel. Êtes-vous sûr de vouloir l'arrêter ? Tapez votre mot de passe actuel pour le confirmer.",
"HTTP Headers": "En-têtes HTTP",
"Trust Proxy": "Proxy de confiance",
"Other Software": "Autres logiciels",
"For example: nginx, Apache and Traefik.": "Par exemple : nginx, Apache et Traefik.",
"Please read": "S'il vous plaît Lisez",
"Valid To:": "Valable pour:",
"Days Remaining:": "Jours restant:",
"Domain Name Expiry Notification": "Notification d'expiration de nom de domaine",
"Date Created": "Date de création",
HomeAssistant: "Home Assistant",
onebotHttpAddress: "Adresse HTTP OneBot",
onebotMessageType: "Type de message OneBot",
onebotGroupMessage: "Groupe",
onebotUserOrGroupId: "ID de groupe/utilisateur",
onebotSafetyTips: "Pour des raisons de sécurité, vous devez définir un jeton d'accès",
"PushDeer Key": "Clé PushDeer",
"Show Powered By": "Afficher \"Propulsé par\"",
RadiusSecretDescription: "Secret partagé entre le client et le serveur",
RadiusCalledStationId: "Identifiant de la station appelée",
RadiusCalledStationIdDescription: "Identifiant de l'appareil appelé",
RadiusCallingStationId: "Identifiant de la station appelante",
RadiusCallingStationIdDescription: "Identifiant de l'appareil appelant",
"Certificate Expiry Notification": "Notification d'expiration du certificat",
"API Username": "Nom d'utilisateur de l'API",
"API Key": "clé API",
"Recipient Number": "Numéro du destinataire",
"From Name/Number": "De Nom/Numéro",
"Leave blank to use a shared sender number.": "Laisser vide pour utiliser un numéro d'expéditeur partagé.",
"Octopush API Version": "Version de l'API Octopush",
octopushAPIKey: "\"Clé API\" à partir des informations d'identification de l'API HTTP dans le panneau de configuration",
octopushLogin: "\"Connexion\" à partir des informations d'identification de l'API HTTP dans le panneau de configuration",
"Using a Reverse Proxy?": "Utiliser un proxy inverse ?",
"Check how to config it for WebSocket": "Vérifiez comment le configurer pour WebSocket",
wayToGetClickSendSMSToken: "Vous pouvez obtenir le nom d'utilisateur API et la clé API à partir de {0} .",
"Connection String": "Chaîne de connexion",
Query: "Requête",
tcp: "TCP / HTTP",
"Docker Container": "Conteneur Docker",
Workstation: "Poste de travail",
disableCloudflaredNoAuthMsg: "Vous êtes en mode No Auth, un mot de passe n'est pas nécessaire.",
"Long-Lived Access Token": "Jeton d'accès de longue durée",
"Then choose an action, for example switch the scene to where an RGB light is red.": "Ensuite, choisissez une action, par exemple basculer la scène là où une lumière RVB est rouge.",
"Frontend Version": "Frontend Version",
"Frontend Version do not match backend version!": "La version frontale ne correspond pas à la version principale !",
"Base URL": "URL de base",
goAlertInfo: "GoAlert est une application open source pour la planification des appels, les escalades automatisées et les notifications (comme les SMS ou les appels vocaux). Engagez automatiquement la bonne personne, de la bonne manière et au bon moment ! {0}",
goAlertIntegrationKeyInfo: "Obtenez la clé d'intégration d'API générique pour le service dans ce format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" généralement la valeur du paramètre de jeton de l'URL copiée.",
goAlert: "GoAlert",
backupOutdatedWarning: "Obsolète : étant donné que de nombreuses fonctionnalités ont été ajoutées et que cette fonctionnalité de sauvegarde est un peu non maintenue, elle ne peut pas générer ou restaurer une sauvegarde complète.",
backupRecommend: "Veuillez sauvegarder le volume ou le dossier de données (./data/) directement à la place.",
Optional: "Optionnel",
squadcast: "Squadcast",
};

View File

@@ -280,9 +280,9 @@ function getCryptoRandomInt(min, max) {
}
exports.getCryptoRandomInt = getCryptoRandomInt;
/**
* Generate a secret
* @param length Lenght of secret to generate
* @returns
* Generate a random alphanumeric string of fixed length
* @param length Length of string to generate
* @returns string
*/
function genSecret(length = 64) {
let secret = "";

View File

@@ -1,7 +1,11 @@
const { genSecret, DOWN } = require("../src/util");
const { genSecret, DOWN, log} = require("../src/util");
const utilServerRewire = require("../server/util-server");
const Discord = require("../server/notification-providers/discord");
const axios = require("axios");
const { UptimeKumaServer } = require("../server/uptime-kuma-server");
const Database = require("../server/database");
const {Settings} = require("../server/settings");
const fs = require("fs");
jest.mock("axios");
@@ -225,3 +229,80 @@ describe("The function filterAndJoin", () => {
expect(result).toBe("");
});
});
describe("Test uptimeKumaServer.getClientIP()", () => {
it("should able to get a correct client IP", async () => {
Database.init({
"data-dir": "./data/test"
});
if (! fs.existsSync(Database.path)) {
log.info("server", "Copying Database");
fs.copyFileSync(Database.templatePath, Database.path);
}
await Database.connect(true);
await Database.patch();
const fakeSocket = {
client: {
conn: {
remoteAddress: "192.168.10.10",
request: {
headers: {
}
}
}
}
}
const server = Object.create(UptimeKumaServer.prototype);
let ip = await server.getClientIP(fakeSocket);
await Settings.set("trustProxy", false);
expect(await Settings.get("trustProxy")).toBe(false);
expect(ip).toBe("192.168.10.10");
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "10.10.10.10";
ip = await server.getClientIP(fakeSocket);
expect(ip).toBe("192.168.10.10");
fakeSocket.client.conn.request.headers["x-real-ip"] = "20.20.20.20";
ip = await server.getClientIP(fakeSocket);
expect(ip).toBe("192.168.10.10");
await Settings.set("trustProxy", true);
expect(await Settings.get("trustProxy")).toBe(true);
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "10.10.10.10";
ip = await server.getClientIP(fakeSocket);
expect(ip).toBe("10.10.10.10");
// x-real-ip
delete fakeSocket.client.conn.request.headers["x-forwarded-for"];
ip = await server.getClientIP(fakeSocket);
expect(ip).toBe("20.20.20.20");
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "2001:db8:85a3:8d3:1319:8a2e:370:7348";
ip = await server.getClientIP(fakeSocket);
expect(ip).toBe("2001:db8:85a3:8d3:1319:8a2e:370:7348");
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "203.0.113.195";
ip = await server.getClientIP(fakeSocket);
expect(ip).toBe("203.0.113.195");
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348";
ip = await server.getClientIP(fakeSocket);
expect(ip).toBe("203.0.113.195");
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178";
ip = await server.getClientIP(fakeSocket);
expect(ip).toBe("203.0.113.195");
// Elements are comma-separated, with optional whitespace surrounding the commas.
fakeSocket.client.conn.request.headers["x-forwarded-for"] = "203.0.113.195 , 2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178";
ip = await server.getClientIP(fakeSocket);
expect(ip).toBe("203.0.113.195");
await Database.close();
}, 120000);
});

View File

@@ -0,0 +1,18 @@
const actor = require("../support/actors/actor");
const userData = require("../support/const/user-data");
const dashboardPage = require("../support/pages/dashboard-page");
const setupPage = require("../support/pages/setup-page");
describe("user can create a new account on setup page", () => {
before(() => {
cy.visit("/setup");
});
it("user can create new account", () => {
cy.url().should("be.equal", setupPage.SetupPage.url);
actor.actor.setupTask.fillAndSubmitSetupForm(userData.DEFAULT_USER_DATA.username, userData.DEFAULT_USER_DATA.password, userData.DEFAULT_USER_DATA.password);
cy.url().should("be.equal", dashboardPage.DashboardPage.url);
cy.get('[role="alert"]')
.should("be.visible")
.and("contain.text", "Added Successfully.");
});
});

View File

@@ -0,0 +1,8 @@
const setupTask = require("../tasks/setup-task");
class Actor {
constructor() {
this.setupTask = new setupTask.SetupTask();
}
}
const actor = new Actor();
exports.actor = actor;

View File

@@ -1,4 +1,4 @@
export const DEFAULT_USER_DATA = {
exports.DEFAULT_USER_DATA = {
username: "testuser",
password: "testuser123",
};

View File

@@ -0,0 +1 @@
require("./commands");

View File

@@ -1,3 +1,3 @@
export const DashboardPage = {
exports.DashboardPage = {
url: Cypress.env("baseUrl") + "/dashboard",
};

View File

@@ -1,4 +1,4 @@
export const SetupPage = {
exports.SetupPage = {
url: Cypress.env("baseUrl") + "/setup",
usernameInput: '[data-cy="username-input"]',
passWordInput: '[data-cy="password-input"]',

View File

@@ -0,0 +1,11 @@
const setupPage = require("../pages/setup-page");
class SetupTask {
fillAndSubmitSetupForm(username, password, passwordRepeat) {
cy.get(setupPage.SetupPage.usernameInput).type(username);
cy.get(setupPage.SetupPage.passWordInput).type(password);
cy.get(setupPage.SetupPage.passwordRepeatInput).type(passwordRepeat);
cy.get(setupPage.SetupPage.submitSetupForm).click();
}
}
exports.SetupTask = SetupTask;

View File

@@ -1,329 +0,0 @@
// eslint-disable-next-line no-unused-vars
const { Page, Browser } = require("puppeteer");
const { sleep } = require("../src/util");
/**
* Set back the correct data type for page object
* @type {Page}
*/
page;
/**
* @type {Browser}
*/
browser;
beforeAll(async () => {
await page.setViewport({
width: 1280,
height: 720,
deviceScaleFactor: 1,
});
});
afterAll(() => {
});
const baseURL = "http://127.0.0.1:3002";
describe("Init", () => {
const title = "Uptime Kuma";
beforeAll(async () => {
await page.goto(baseURL);
});
it(`should be titled "${title}"`, async () => {
await expect(page.title()).resolves.toEqual(title);
});
// Setup Page
it("Setup", async () => {
// Create an Admin
await page.waitForSelector("#floatingInput");
await page.waitForSelector("#repeat");
await page.click("#floatingInput");
await page.type("#floatingInput", "admin");
await page.type("#floatingPassword", "admin123");
await page.type("#repeat", "admin123");
await page.click(".btn-primary[type=submit]");
await sleep(3000);
// Go to /setup again
await page.goto(baseURL + "/setup");
await sleep(3000);
let pathname = await page.evaluate(() => location.pathname);
expect(pathname).toEqual("/dashboard");
// Go to /
await page.goto(baseURL);
await page.waitForSelector("h1.mb-3");
pathname = await page.evaluate(() => location.pathname);
expect(pathname).toEqual("/dashboard");
});
it("should create monitor", async () => {
// Create monitor
await page.goto(baseURL + "/add");
await page.waitForSelector("#name");
await page.type("#name", "Myself");
await page.waitForSelector("#url");
await page.click("#url", { clickCount: 3 });
await page.keyboard.type(baseURL);
await page.keyboard.press("Enter");
await page.waitForFunction(() => {
const badge = document.querySelector("span.badge");
return badge && badge.innerText == "100%";
}, { timeout: 5000 });
});
// Settings Page
/*
describe("Settings", () => {
beforeEach(async () => {
await page.goto(baseURL + "/settings");
});
it("Change Language", async () => {
await page.goto(baseURL + "/settings/appearance");
await page.waitForSelector("#language");
await page.select("#language", "zh-HK");
let languageTitle = await page.evaluate(() => document.querySelector("[for=language]").innerText);
expect(languageTitle).toEqual("語言");
await page.select("#language", "en");
languageTitle = await page.evaluate(() => document.querySelector("[for=language]").innerText);
expect(languageTitle).toEqual("Language");
});
it("Change Theme", async () => {
await page.goto(baseURL + "/settings/appearance");
// Dark
await click(page, ".btn[for=btncheck2]");
await page.waitForSelector("div.dark");
await page.waitForSelector(".btn[for=btncheck1]");
// Light
await click(page, ".btn[for=btncheck1]");
await page.waitForSelector("div.light");
});
it("Change Heartbeat Bar Style", async () => {
await page.goto(baseURL + "/settings/appearance");
// Bottom
await click(page, ".btn[for=btncheck5]");
await page.waitForSelector("div.hp-bar-big");
// None
await click(page, ".btn[for=btncheck6]");
await page.waitForSelector("div.hp-bar-big", {
hidden: true,
timeout: 1000
});
});
// TODO: Timezone
it("Search Engine Visibility", async () => {
// Default
let res = await axios.get(baseURL + "/robots.txt");
expect(res.data).toContain("Disallow: /");
// Yes
await click(page, "#searchEngineIndexYes");
await click(page, "form > div > .btn[type=submit]");
await sleep(1000);
res = await axios.get(baseURL + "/robots.txt");
expect(res.data).not.toContain("Disallow: /");
// No
await click(page, "#searchEngineIndexNo");
await click(page, "form > div > .btn[type=submit]");
await sleep(1000);
res = await axios.get(baseURL + "/robots.txt");
expect(res.data).toContain("Disallow: /");
});
it("Entry Page", async () => {
const newPage = await browser.newPage();
// Default
await newPage.goto(baseURL);
await newPage.waitForSelector("h1.mb-3", { timeout: 3000 });
let pathname = await newPage.evaluate(() => location.pathname);
expect(pathname).toEqual("/dashboard");
// Status Page
await click(page, "#entryPageNo");
await click(page, "form > div > .btn[type=submit]");
await sleep(1000);
await newPage.goto(baseURL);
await newPage.waitForSelector("img.logo", { timeout: 3000 });
pathname = await newPage.evaluate(() => location.pathname);
expect(pathname).toEqual("/status");
// Back to Dashboard
await click(page, "#entryPageYes");
await click(page, "form > div > .btn[type=submit]");
await sleep(1000);
await newPage.goto(baseURL);
await newPage.waitForSelector("h1.mb-3", { timeout: 3000 });
pathname = await newPage.evaluate(() => location.pathname);
expect(pathname).toEqual("/dashboard");
await newPage.close();
});
it("Change Password (wrong current password)", async () => {
await page.goto(baseURL + "/settings/security");
await page.waitForSelector("#current-password");
await page.type("#current-password", "wrong_passw$$d");
await page.type("#new-password", "new_password123");
await page.type("#repeat-new-password", "new_password123");
// Save
await click(page, "form > div > .btn[type=submit]", 0);
await sleep(1000);
await click(page, "#logout-btn");
await login("admin", "new_password123");
let elementCount = await page.evaluate(() => document.querySelectorAll("#floatingPassword").length);
expect(elementCount).toEqual(1);
await login("admin", "admin123");
});
it("Change Password (wrong repeat)", async () => {
await page.goto(baseURL + "/settings/security");
await page.waitForSelector("#current-password");
await page.type("#current-password", "admin123");
await page.type("#new-password", "new_password123");
await page.type("#repeat-new-password", "new_password1234567898797898");
await click(page, "form > div > .btn[type=submit]", 0);
await sleep(1000);
await click(page, "#logout-btn");
await login("admin", "new_password123");
let elementCount = await page.evaluate(() => document.querySelectorAll("#floatingPassword").length);
expect(elementCount).toEqual(1);
await login("admin", "admin123");
await page.waitForSelector("#current-password");
let pathname = await page.evaluate(() => location.pathname);
expect(pathname).toEqual("/settings/security");
});
// TODO: 2FA
// TODO: Export Backup
// TODO: Import Backup
it("Should disable & enable auth", async () => {
await page.goto(baseURL + "/settings/security");
await click(page, "#disableAuth-btn");
await click(page, ".btn.btn-danger[data-bs-dismiss='modal']", 2); // Not a good way to do it
await page.waitForSelector("#enableAuth-btn", { timeout: 3000 });
await page.waitForSelector("#logout-btn", {
hidden: true,
timeout: 3000
});
const newPage = await browser.newPage();
await newPage.goto(baseURL);
await newPage.waitForSelector("span.badge", { timeout: 3000 });
newPage.close();
await click(page, "#enableAuth-btn");
await login("admin", "admin123");
await page.waitForSelector("#disableAuth-btn", { timeout: 3000 });
});
// it("Should clear all statistics", async () => {
// await page.goto(baseURL + "/settings/monitor-history");
// await click(page, "#clearAllStats-btn");
// await click(page, ".btn.btn-danger");
// await page.waitForFunction(() => {
// const badge = document.querySelector("span.badge");
// return badge && badge.innerText == "0%";
// }, { timeout: 3000 });
// });
});
*/
/*
* TODO
* Create Monitor - All type
* Edit Monitor
* Delete Monitor
*
* Create Notification (token problem, maybe hard to test)
*
*/
describe("Status Page", () => {
const title = "Uptime Kuma";
beforeAll(async () => {
await page.goto(baseURL + "/status");
});
it(`should be titled "${title}"`, async () => {
await expect(page.title()).resolves.toEqual(title);
});
});
});
/**
* Test login
* @param {string} username
* @param {string} password
*/
async function login(username, password) {
await input(page, "#floatingInput", username);
await input(page, "#floatingPassword", password);
await page.click(".btn-primary[type=submit]");
await sleep(5000);
}
/**
* Click on an element on the page
* @param {Page} page Puppeteer page instance
* @param {string} selector
* @param {number} elementIndex
* @returns {Promise<any>}
*/
async function click(page, selector, elementIndex = 0) {
await page.waitForSelector(selector, {
timeout: 5000,
});
return await page.evaluate((s, i) => {
return document.querySelectorAll(s)[i].click();
}, selector, elementIndex);
}
/**
* Input text into selected field
* @param {Page} page Puppeteer page instance
* @param {string} selector
* @param {string} text Text to input
*/
async function input(page, selector, text) {
await page.waitForSelector(selector, {
timeout: 5000,
});
const element = await page.$(selector);
await element.click({ clickCount: 3 });
await page.keyboard.press("Backspace");
await page.type(selector, text);
}

View File

@@ -1,42 +0,0 @@
// eslint-disable-next-line no-global-assign
global.localStorage = {};
global.navigator = {
language: "en"
};
const { currentLocale } = require("../src/i18n");
describe("Test i18n.js", () => {
it("currentLocale()", () => {
expect(currentLocale()).toEqual("en");
navigator.language = "zh-HK";
expect(currentLocale()).toEqual("zh-HK");
// Note that in Safari on iOS prior to 10.2, the country code returned is lowercase: "en-us", "fr-fr" etc.
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language
navigator.language = "zh-hk";
expect(currentLocale()).toEqual("en");
navigator.language = "en-US";
expect(currentLocale()).toEqual("en");
navigator.language = "ja-ZZ";
expect(currentLocale()).toEqual("ja");
navigator.language = "zz";
expect(currentLocale()).toEqual("en");
navigator.language = "zz-ZZ";
expect(currentLocale()).toEqual("en");
localStorage.locale = "en";
expect(currentLocale()).toEqual("en");
localStorage.locale = "zh-HK";
expect(currentLocale()).toEqual("zh-HK");
});
});

View File

@@ -1,10 +0,0 @@
const fs = require("fs");
const rmSync = require("../extra/fs-rmSync.js");
const path = "./data/test-chrome-profile";
if (fs.existsSync(path)) {
rmSync(path, {
recursive: true,
});
}

View File

@@ -11,11 +11,9 @@
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": false,
"strict": true,
"types": ["cypress"]
"strict": true
},
"files": [
"./src/util.ts",
],
"include": ["cypress/**/*.ts"]
"./src/util.ts"
]
}