Add error class for config file issues (#14495)
This commit is contained in:
parent
71ac9e270a
commit
bcb29b3c38
24 changed files with 235 additions and 137 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import { reactive, watch } from "vue";
|
||||
import api from "./api";
|
||||
import store from "./store";
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import { isSystemError } from "./utils/fatal";
|
||||
|
||||
const auth = reactive({
|
||||
configured: true,
|
||||
|
|
@ -9,6 +11,11 @@ const auth = reactive({
|
|||
});
|
||||
|
||||
export async function updateAuthStatus() {
|
||||
if (store.state.offline || isSystemError(store.state.fatal)) {
|
||||
// system not ready, skip auth check
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await api.get("/auth/status", {
|
||||
validateStatus: (code) => [200, 501, 500].includes(code),
|
||||
|
|
@ -72,4 +79,15 @@ watch(
|
|||
}
|
||||
);
|
||||
|
||||
let timeoutId = null;
|
||||
function debounedUpdateAuthStatus() {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
updateAuthStatus();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
watch(() => store.state.offline, debounedUpdateAuthStatus);
|
||||
watch(() => store.state.fatal, debounedUpdateAuthStatus);
|
||||
|
||||
export default auth;
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ import collector from "../mixins/collector";
|
|||
import { logout, isLoggedIn, openLoginModal } from "../auth";
|
||||
import baseAPI from "../baseapi";
|
||||
import { isApp, sendToApp } from "../utils/native";
|
||||
import { isUserConfigError } from "../utils/fatal";
|
||||
|
||||
export default {
|
||||
name: "TopNavigation",
|
||||
|
|
@ -168,7 +169,8 @@ export default {
|
|||
return this.logoutCount > 0;
|
||||
},
|
||||
showBadge() {
|
||||
return this.loginRequired || this.sponsor.expiresSoon || this.fatal?.error;
|
||||
const userConfigError = isUserConfigError(this.fatal);
|
||||
return this.loginRequired || this.sponsor.expiresSoon || userConfigError;
|
||||
},
|
||||
badgeClass() {
|
||||
if (this.fatal?.error) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { createRouter, createWebHashHistory } from "vue-router";
|
|||
import Modal from "bootstrap/js/dist/modal";
|
||||
import Main from "./views/Main.vue";
|
||||
import { ensureCurrentLocaleMessages } from "./i18n";
|
||||
import { openLoginModal, statusUnknown, updateAuthStatus, isLoggedIn } from "./auth";
|
||||
import { openLoginModal, statusUnknown, updateAuthStatus, isLoggedIn, isConfigured } from "./auth";
|
||||
|
||||
function hideAllModals() {
|
||||
[...document.querySelectorAll(".modal.show")].forEach((modal) => {
|
||||
|
|
@ -18,6 +18,9 @@ function hideAllModals() {
|
|||
|
||||
async function ensureAuth(to) {
|
||||
await updateAuthStatus();
|
||||
if (!isConfigured()) {
|
||||
return false;
|
||||
}
|
||||
if (!isLoggedIn() && !statusUnknown()) {
|
||||
openLoginModal(to.path);
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -19,11 +19,15 @@ function setProperty(obj, props, value) {
|
|||
}
|
||||
|
||||
const state = reactive({
|
||||
offline: false,
|
||||
loadpoints: [], // ensure array type
|
||||
});
|
||||
|
||||
const store = {
|
||||
state,
|
||||
offline: function (value) {
|
||||
state.offline = value;
|
||||
},
|
||||
update: function (msg) {
|
||||
Object.keys(msg).forEach(function (k) {
|
||||
if (k === "log") {
|
||||
|
|
|
|||
21
assets/js/utils/fatal.js
Normal file
21
assets/js/utils/fatal.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
const FATALS = ["configfile", "database"];
|
||||
|
||||
function isError(fatal) {
|
||||
return !!fatal?.error;
|
||||
}
|
||||
|
||||
export function isUserConfigError(fatal) {
|
||||
if (!isError(fatal)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const errorClass = fatal?.class;
|
||||
if (FATALS.includes(errorClass)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isSystemError(fatal) {
|
||||
return isError(fatal) && !isUserConfigError(fatal);
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ import PasswordModal from "../components/PasswordModal.vue";
|
|||
import LoginModal from "../components/LoginModal.vue";
|
||||
import HelpModal from "../components/HelpModal.vue";
|
||||
import collector from "../mixins/collector";
|
||||
import { updateAuthStatus } from "../auth";
|
||||
|
||||
// assume offline if not data received for 5 minutes
|
||||
let lastDataReceived = new Date();
|
||||
|
|
@ -61,9 +60,9 @@ export default {
|
|||
this.reload();
|
||||
}
|
||||
},
|
||||
offline: function (now, prev) {
|
||||
updateAuthStatus();
|
||||
if (now && !prev) {
|
||||
offline: function (offline) {
|
||||
store.offline(offline);
|
||||
if (offline) {
|
||||
this.reconnect();
|
||||
}
|
||||
},
|
||||
|
|
@ -88,7 +87,6 @@ export default {
|
|||
mounted: function () {
|
||||
this.connect();
|
||||
document.addEventListener("visibilitychange", this.pageVisibilityChanged, false);
|
||||
updateAuthStatus();
|
||||
},
|
||||
unmounted: function () {
|
||||
this.disconnect();
|
||||
|
|
@ -102,7 +100,6 @@ export default {
|
|||
this.disconnect();
|
||||
} else {
|
||||
this.connect();
|
||||
updateAuthStatus();
|
||||
}
|
||||
},
|
||||
reconnect: function () {
|
||||
|
|
@ -113,7 +110,6 @@ export default {
|
|||
}, 2500);
|
||||
},
|
||||
disconnect: function () {
|
||||
console.log("websocket disconnecting");
|
||||
if (this.ws) {
|
||||
this.ws.onerror = null;
|
||||
this.ws.onopen = null;
|
||||
|
|
@ -150,7 +146,7 @@ export default {
|
|||
|
||||
this.ws = new WebSocket(uri);
|
||||
this.ws.onerror = () => {
|
||||
console.error({ message: "Websocket error. Trying to reconnect." });
|
||||
console.log({ message: "Websocket error. Trying to reconnect." });
|
||||
this.ws.close();
|
||||
};
|
||||
this.ws.onopen = () => {
|
||||
|
|
@ -158,8 +154,8 @@ export default {
|
|||
window.app.setOnline();
|
||||
};
|
||||
this.ws.onclose = () => {
|
||||
console.log("websocket disconnected");
|
||||
window.app.setOffline();
|
||||
this.reconnect();
|
||||
};
|
||||
this.ws.onmessage = (evt) => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
const _ClassName = "meterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsinfluxmessengersponsorship"
|
||||
const _ClassName = "configfilemeterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsinfluxmessengersponsorship"
|
||||
|
||||
var _ClassIndex = [...]uint8{0, 5, 12, 19, 25, 32, 36, 40, 48, 59, 64, 74, 76, 80, 86, 95, 106}
|
||||
var _ClassIndex = [...]uint8{0, 10, 15, 22, 29, 35, 42, 46, 50, 58, 69, 74, 84, 86, 90, 96, 105, 116}
|
||||
|
||||
const _ClassLowerName = "meterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsinfluxmessengersponsorship"
|
||||
const _ClassLowerName = "configfilemeterchargervehicletariffcircuitsitemqttdatabasemodbusproxyeebusjavascriptgohemsinfluxmessengersponsorship"
|
||||
|
||||
func (i Class) String() string {
|
||||
i -= 1
|
||||
|
|
@ -25,78 +25,82 @@ func (i Class) String() string {
|
|||
// Re-run the stringer command to generate them again.
|
||||
func _ClassNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[ClassMeter-(1)]
|
||||
_ = x[ClassCharger-(2)]
|
||||
_ = x[ClassVehicle-(3)]
|
||||
_ = x[ClassTariff-(4)]
|
||||
_ = x[ClassCircuit-(5)]
|
||||
_ = x[ClassSite-(6)]
|
||||
_ = x[ClassMqtt-(7)]
|
||||
_ = x[ClassDatabase-(8)]
|
||||
_ = x[ClassModbusProxy-(9)]
|
||||
_ = x[ClassEEBus-(10)]
|
||||
_ = x[ClassJavascript-(11)]
|
||||
_ = x[ClassGo-(12)]
|
||||
_ = x[ClassHEMS-(13)]
|
||||
_ = x[ClassInflux-(14)]
|
||||
_ = x[ClassMessenger-(15)]
|
||||
_ = x[ClassSponsorship-(16)]
|
||||
_ = x[ClassConfigFile-(1)]
|
||||
_ = x[ClassMeter-(2)]
|
||||
_ = x[ClassCharger-(3)]
|
||||
_ = x[ClassVehicle-(4)]
|
||||
_ = x[ClassTariff-(5)]
|
||||
_ = x[ClassCircuit-(6)]
|
||||
_ = x[ClassSite-(7)]
|
||||
_ = x[ClassMqtt-(8)]
|
||||
_ = x[ClassDatabase-(9)]
|
||||
_ = x[ClassModbusProxy-(10)]
|
||||
_ = x[ClassEEBus-(11)]
|
||||
_ = x[ClassJavascript-(12)]
|
||||
_ = x[ClassGo-(13)]
|
||||
_ = x[ClassHEMS-(14)]
|
||||
_ = x[ClassInflux-(15)]
|
||||
_ = x[ClassMessenger-(16)]
|
||||
_ = x[ClassSponsorship-(17)]
|
||||
}
|
||||
|
||||
var _ClassValues = []Class{ClassMeter, ClassCharger, ClassVehicle, ClassTariff, ClassCircuit, ClassSite, ClassMqtt, ClassDatabase, ClassModbusProxy, ClassEEBus, ClassJavascript, ClassGo, ClassHEMS, ClassInflux, ClassMessenger, ClassSponsorship}
|
||||
var _ClassValues = []Class{ClassConfigFile, ClassMeter, ClassCharger, ClassVehicle, ClassTariff, ClassCircuit, ClassSite, ClassMqtt, ClassDatabase, ClassModbusProxy, ClassEEBus, ClassJavascript, ClassGo, ClassHEMS, ClassInflux, ClassMessenger, ClassSponsorship}
|
||||
|
||||
var _ClassNameToValueMap = map[string]Class{
|
||||
_ClassName[0:5]: ClassMeter,
|
||||
_ClassLowerName[0:5]: ClassMeter,
|
||||
_ClassName[5:12]: ClassCharger,
|
||||
_ClassLowerName[5:12]: ClassCharger,
|
||||
_ClassName[12:19]: ClassVehicle,
|
||||
_ClassLowerName[12:19]: ClassVehicle,
|
||||
_ClassName[19:25]: ClassTariff,
|
||||
_ClassLowerName[19:25]: ClassTariff,
|
||||
_ClassName[25:32]: ClassCircuit,
|
||||
_ClassLowerName[25:32]: ClassCircuit,
|
||||
_ClassName[32:36]: ClassSite,
|
||||
_ClassLowerName[32:36]: ClassSite,
|
||||
_ClassName[36:40]: ClassMqtt,
|
||||
_ClassLowerName[36:40]: ClassMqtt,
|
||||
_ClassName[40:48]: ClassDatabase,
|
||||
_ClassLowerName[40:48]: ClassDatabase,
|
||||
_ClassName[48:59]: ClassModbusProxy,
|
||||
_ClassLowerName[48:59]: ClassModbusProxy,
|
||||
_ClassName[59:64]: ClassEEBus,
|
||||
_ClassLowerName[59:64]: ClassEEBus,
|
||||
_ClassName[64:74]: ClassJavascript,
|
||||
_ClassLowerName[64:74]: ClassJavascript,
|
||||
_ClassName[74:76]: ClassGo,
|
||||
_ClassLowerName[74:76]: ClassGo,
|
||||
_ClassName[76:80]: ClassHEMS,
|
||||
_ClassLowerName[76:80]: ClassHEMS,
|
||||
_ClassName[80:86]: ClassInflux,
|
||||
_ClassLowerName[80:86]: ClassInflux,
|
||||
_ClassName[86:95]: ClassMessenger,
|
||||
_ClassLowerName[86:95]: ClassMessenger,
|
||||
_ClassName[95:106]: ClassSponsorship,
|
||||
_ClassLowerName[95:106]: ClassSponsorship,
|
||||
_ClassName[0:10]: ClassConfigFile,
|
||||
_ClassLowerName[0:10]: ClassConfigFile,
|
||||
_ClassName[10:15]: ClassMeter,
|
||||
_ClassLowerName[10:15]: ClassMeter,
|
||||
_ClassName[15:22]: ClassCharger,
|
||||
_ClassLowerName[15:22]: ClassCharger,
|
||||
_ClassName[22:29]: ClassVehicle,
|
||||
_ClassLowerName[22:29]: ClassVehicle,
|
||||
_ClassName[29:35]: ClassTariff,
|
||||
_ClassLowerName[29:35]: ClassTariff,
|
||||
_ClassName[35:42]: ClassCircuit,
|
||||
_ClassLowerName[35:42]: ClassCircuit,
|
||||
_ClassName[42:46]: ClassSite,
|
||||
_ClassLowerName[42:46]: ClassSite,
|
||||
_ClassName[46:50]: ClassMqtt,
|
||||
_ClassLowerName[46:50]: ClassMqtt,
|
||||
_ClassName[50:58]: ClassDatabase,
|
||||
_ClassLowerName[50:58]: ClassDatabase,
|
||||
_ClassName[58:69]: ClassModbusProxy,
|
||||
_ClassLowerName[58:69]: ClassModbusProxy,
|
||||
_ClassName[69:74]: ClassEEBus,
|
||||
_ClassLowerName[69:74]: ClassEEBus,
|
||||
_ClassName[74:84]: ClassJavascript,
|
||||
_ClassLowerName[74:84]: ClassJavascript,
|
||||
_ClassName[84:86]: ClassGo,
|
||||
_ClassLowerName[84:86]: ClassGo,
|
||||
_ClassName[86:90]: ClassHEMS,
|
||||
_ClassLowerName[86:90]: ClassHEMS,
|
||||
_ClassName[90:96]: ClassInflux,
|
||||
_ClassLowerName[90:96]: ClassInflux,
|
||||
_ClassName[96:105]: ClassMessenger,
|
||||
_ClassLowerName[96:105]: ClassMessenger,
|
||||
_ClassName[105:116]: ClassSponsorship,
|
||||
_ClassLowerName[105:116]: ClassSponsorship,
|
||||
}
|
||||
|
||||
var _ClassNames = []string{
|
||||
_ClassName[0:5],
|
||||
_ClassName[5:12],
|
||||
_ClassName[12:19],
|
||||
_ClassName[19:25],
|
||||
_ClassName[25:32],
|
||||
_ClassName[32:36],
|
||||
_ClassName[36:40],
|
||||
_ClassName[40:48],
|
||||
_ClassName[48:59],
|
||||
_ClassName[59:64],
|
||||
_ClassName[64:74],
|
||||
_ClassName[74:76],
|
||||
_ClassName[76:80],
|
||||
_ClassName[80:86],
|
||||
_ClassName[86:95],
|
||||
_ClassName[95:106],
|
||||
_ClassName[0:10],
|
||||
_ClassName[10:15],
|
||||
_ClassName[15:22],
|
||||
_ClassName[22:29],
|
||||
_ClassName[29:35],
|
||||
_ClassName[35:42],
|
||||
_ClassName[42:46],
|
||||
_ClassName[46:50],
|
||||
_ClassName[50:58],
|
||||
_ClassName[58:69],
|
||||
_ClassName[69:74],
|
||||
_ClassName[74:84],
|
||||
_ClassName[84:86],
|
||||
_ClassName[86:90],
|
||||
_ClassName[90:96],
|
||||
_ClassName[96:105],
|
||||
_ClassName[105:116],
|
||||
}
|
||||
|
||||
// ClassString retrieves an enum value from the enum constants string name.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ type Class int
|
|||
//go:generate enumer -type Class -trimprefix Class -transform=lower -text
|
||||
const (
|
||||
_ Class = iota
|
||||
ClassConfigFile
|
||||
ClassMeter
|
||||
ClassCharger
|
||||
ClassVehicle
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
log.FATAL.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
err = cfgErr
|
||||
err = wrapErrorWithClass(ClassConfigFile, cfgErr)
|
||||
}
|
||||
|
||||
// setup environment
|
||||
|
|
|
|||
26
cmd/setup.go
26
cmd/setup.go
|
|
@ -486,6 +486,17 @@ func configureEnvironment(cmd *cobra.Command, conf *globalconfig.All) (err error
|
|||
request.LogHeaders = true
|
||||
}
|
||||
|
||||
// setup persistence
|
||||
if err == nil {
|
||||
err = wrapErrorWithClass(ClassDatabase, configureDatabase(conf.Database))
|
||||
}
|
||||
|
||||
// setup translations
|
||||
if err == nil {
|
||||
// TODO decide wrapping
|
||||
err = locale.Init()
|
||||
}
|
||||
|
||||
// setup machine id
|
||||
if conf.Plant != "" {
|
||||
// TODO decide wrapping
|
||||
|
|
@ -497,17 +508,6 @@ func configureEnvironment(cmd *cobra.Command, conf *globalconfig.All) (err error
|
|||
err = wrapErrorWithClass(ClassSponsorship, configureSponsorship(conf.SponsorToken))
|
||||
}
|
||||
|
||||
// setup translations
|
||||
if err == nil {
|
||||
// TODO decide wrapping
|
||||
err = locale.Init()
|
||||
}
|
||||
|
||||
// setup persistence
|
||||
if err == nil && conf.Database.Dsn != "" {
|
||||
err = wrapErrorWithClass(ClassDatabase, configureDatabase(conf.Database))
|
||||
}
|
||||
|
||||
// setup mqtt client listener
|
||||
if err == nil {
|
||||
err = wrapErrorWithClass(ClassMqtt, configureMqtt(&conf.Mqtt))
|
||||
|
|
@ -539,6 +539,10 @@ func configureEnvironment(cmd *cobra.Command, conf *globalconfig.All) (err error
|
|||
|
||||
// configureDatabase configures session database
|
||||
func configureDatabase(conf globalconfig.DB) error {
|
||||
if conf.Dsn == "" {
|
||||
return errors.New("database dsn not configured")
|
||||
}
|
||||
|
||||
if err := db.NewInstance(conf.Type, conf.Dsn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default defineConfig({
|
|||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
timeout: 15000, // 15s (default 30s)
|
||||
workers: process.env.CI ? 3 : 8,
|
||||
workers: process.env.CI ? 3 : 4,
|
||||
reporter: "html",
|
||||
use: {
|
||||
baseURL: "http://127.0.0.1:7070",
|
||||
|
|
|
|||
|
|
@ -3,16 +3,12 @@ import { start, stop, baseUrl } from "./evcc";
|
|||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await start("basics.evcc.yaml");
|
||||
await page.goto("/");
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
});
|
||||
const BASIC = "basics.evcc.yaml";
|
||||
|
||||
test("set initial password", async ({ page }) => {
|
||||
await start(BASIC);
|
||||
await page.goto("/");
|
||||
|
||||
const modal = page.getByTestId("password-modal");
|
||||
|
||||
await expect(modal).toBeVisible();
|
||||
|
|
@ -33,14 +29,13 @@ test("set initial password", async ({ page }) => {
|
|||
await modal.getByLabel("Repeat password").fill("secret");
|
||||
await modal.getByRole("button", { name: "Create Password" }).click();
|
||||
await expect(modal).not.toBeVisible();
|
||||
|
||||
await stop();
|
||||
});
|
||||
|
||||
test("login", async ({ page }) => {
|
||||
// set initial password
|
||||
const modal = page.getByTestId("password-modal");
|
||||
await modal.getByLabel("New password").fill("secret");
|
||||
await modal.getByLabel("Repeat password").fill("secret");
|
||||
await modal.getByRole("button", { name: "Create Password" }).click();
|
||||
await start(BASIC, "password.sql");
|
||||
await page.goto("/");
|
||||
|
||||
// go to config
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
|
|
@ -61,14 +56,13 @@ test("login", async ({ page }) => {
|
|||
await login.getByRole("button", { name: "Login" }).click();
|
||||
await expect(login).not.toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible();
|
||||
|
||||
await stop();
|
||||
});
|
||||
|
||||
test("http iframe hint", async ({ page }) => {
|
||||
// set initial password
|
||||
const modal = page.getByTestId("password-modal");
|
||||
await modal.getByLabel("New password").fill("secret");
|
||||
await modal.getByLabel("Repeat password").fill("secret");
|
||||
await modal.getByRole("button", { name: "Create Password" }).click();
|
||||
await start(BASIC, "password.sql");
|
||||
await page.goto("/");
|
||||
|
||||
// go to config
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
|
|
@ -90,26 +84,27 @@ test("http iframe hint", async ({ page }) => {
|
|||
|
||||
// iframe hint visible (login-iframe-hint)
|
||||
await expect(login.getByTestId("login-iframe-hint")).toBeVisible();
|
||||
|
||||
await stop();
|
||||
});
|
||||
|
||||
test("update password", async ({ page }) => {
|
||||
const instance = await start(BASIC, "password.sql");
|
||||
await page.goto("/");
|
||||
|
||||
const oldPassword = "secret";
|
||||
const newPassword = "newsecret";
|
||||
|
||||
// set initial password
|
||||
const modal = page.getByTestId("password-modal");
|
||||
await modal.getByLabel("New password").fill(oldPassword);
|
||||
await modal.getByLabel("Repeat password").fill(oldPassword);
|
||||
await modal.getByRole("button", { name: "Create Password" }).click();
|
||||
|
||||
// login modal
|
||||
page.goto("/#/config");
|
||||
const loginOld = page.getByTestId("login-modal");
|
||||
await loginOld.getByLabel("Password").fill(oldPassword);
|
||||
await loginOld.getByRole("button", { name: "Login" }).click();
|
||||
await expect(loginOld).not.toBeVisible();
|
||||
|
||||
// update password
|
||||
await page.getByTestId("generalconfig-password").getByRole("button", { name: "edit" }).click();
|
||||
const modal = page.getByTestId("password-modal");
|
||||
await expect(modal.getByRole("heading", { name: "Update Administrator Password" })).toBeVisible();
|
||||
await modal.getByLabel("Current password").fill(oldPassword);
|
||||
await modal.getByLabel("New password").fill(newPassword);
|
||||
|
|
@ -133,11 +128,6 @@ test("update password", async ({ page }) => {
|
|||
await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible();
|
||||
await expect(loginNew).not.toBeVisible();
|
||||
|
||||
// revert password
|
||||
await page.getByTestId("generalconfig-password").getByRole("button", { name: "edit" }).click();
|
||||
await modal.getByLabel("Current password").fill(newPassword);
|
||||
await modal.getByLabel("New password").fill(oldPassword);
|
||||
await modal.getByLabel("Repeat password").fill(oldPassword);
|
||||
await modal.getByRole("button", { name: "Update Password" }).click();
|
||||
await expect(page.getByTestId("password-modal")).not.toBeVisible();
|
||||
// hard stop, since password is updated
|
||||
await stop(instance);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ test.afterAll(async () => {
|
|||
async function login(page) {
|
||||
await page.locator("#loginPassword").fill("secret");
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
await expect(page.locator("#loginPassword")).not.toBeVisible();
|
||||
}
|
||||
|
||||
async function enableExperimental(page) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ test.afterAll(async () => {
|
|||
async function login(page) {
|
||||
await page.locator("#loginPassword").fill("secret");
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
await expect(page.locator("#loginPassword")).not.toBeVisible();
|
||||
}
|
||||
|
||||
async function enableExperimental(page) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ test.afterEach(async () => {
|
|||
async function login(page) {
|
||||
await page.locator("#loginPassword").fill("secret");
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
await expect(page.locator("#loginPassword")).not.toBeVisible();
|
||||
}
|
||||
|
||||
async function enableExperimental(page) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const SELECT_ALL = "ControlOrMeta+KeyA";
|
|||
async function login(page) {
|
||||
await page.locator("#loginPassword").fill("secret");
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
await expect(page.locator("#loginPassword")).not.toBeVisible();
|
||||
}
|
||||
|
||||
async function enableExperimental(page) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ test.afterAll(async () => {
|
|||
async function login(page) {
|
||||
await page.locator("#loginPassword").fill("secret");
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
await expect(page.locator("#loginPassword")).not.toBeVisible();
|
||||
}
|
||||
|
||||
async function enableExperimental(page) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ test.afterAll(async () => {
|
|||
async function login(page) {
|
||||
await page.locator("#loginPassword").fill("secret");
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
await expect(page.locator("#loginPassword")).not.toBeVisible();
|
||||
}
|
||||
|
||||
async function enableExperimental(page) {
|
||||
|
|
|
|||
|
|
@ -7,17 +7,21 @@ import path from "path";
|
|||
|
||||
const BINARY = "./evcc";
|
||||
|
||||
function port() {
|
||||
function workerPort() {
|
||||
const index = process.env.TEST_WORKER_INDEX * 1;
|
||||
return 11000 + index;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function baseUrl() {
|
||||
return `http://localhost:${port()}`;
|
||||
return `http://localhost:${workerPort()}`;
|
||||
}
|
||||
|
||||
function dbPath() {
|
||||
const file = `evcc-${port()}.db`;
|
||||
const file = `evcc-${workerPort()}.db`;
|
||||
return path.join(os.tmpdir(), file);
|
||||
}
|
||||
|
||||
|
|
@ -26,11 +30,11 @@ export async function start(config, sqlDumps) {
|
|||
if (sqlDumps) {
|
||||
await _restoreDatabase(sqlDumps);
|
||||
}
|
||||
await _start(config);
|
||||
return await _start(config);
|
||||
}
|
||||
|
||||
export async function stop() {
|
||||
await _stop();
|
||||
export async function stop(instance) {
|
||||
await _stop(instance);
|
||||
await _clean();
|
||||
}
|
||||
|
||||
|
|
@ -58,27 +62,39 @@ async function _restoreDatabase(sqlDumps) {
|
|||
|
||||
async function _start(config) {
|
||||
const configFile = config.includes("/") ? config : `tests/${config}`;
|
||||
console.log("starting evcc", { config });
|
||||
const port = workerPort();
|
||||
console.log(`wait until port ${port} is available`);
|
||||
await waitOn({ resources: [`tcp:localhost:${port}`], reverse: true });
|
||||
console.log("starting evcc", { config, port });
|
||||
const instance = exec(
|
||||
`EVCC_NETWORK_PORT=${port()} EVCC_DATABASE_DSN=${dbPath()} ${BINARY} --config ${configFile}`
|
||||
`EVCC_NETWORK_PORT=${port} EVCC_DATABASE_DSN=${dbPath()} ${BINARY} --config ${configFile}`
|
||||
);
|
||||
instance.stdout.pipe(process.stdout);
|
||||
instance.stderr.pipe(process.stderr);
|
||||
instance.on("exit", (code) => {
|
||||
console.log("evcc terminated", code);
|
||||
console.log("evcc terminated", { code, port, config });
|
||||
});
|
||||
await waitOn({ resources: [baseUrl()] });
|
||||
return instance;
|
||||
}
|
||||
|
||||
async function _stop() {
|
||||
console.log("shutting down evcc");
|
||||
async function _stop(instance) {
|
||||
if (instance) {
|
||||
console.log("shutting down evcc hard");
|
||||
// hard kill, only use of normal shutdown doesn't work
|
||||
instance.kill("SIGKILL");
|
||||
await sleep(300);
|
||||
return;
|
||||
}
|
||||
const port = workerPort();
|
||||
console.log("shutting down evcc", { port });
|
||||
const res = await axios.post(`${baseUrl()}/api/auth/login`, { password: "secret" });
|
||||
console.log(res.status, res.statusText);
|
||||
const cookie = res.headers["set-cookie"];
|
||||
await axios.post(`${baseUrl()}/api/system/shutdown`, {}, { headers: { cookie } });
|
||||
console.log("wait until network port is closed");
|
||||
await waitOn({ resources: [`tcp:localhost:${port()}`], reverse: true });
|
||||
console.log("evcc is down");
|
||||
console.log(`wait until port ${port} is closed`);
|
||||
await waitOn({ resources: [`tcp:localhost:${port}`], reverse: true });
|
||||
console.log("evcc is down", { port });
|
||||
}
|
||||
|
||||
async function _clean() {
|
||||
|
|
|
|||
6
tests/fatal-db.evcc.yaml
Executable file
6
tests/fatal-db.evcc.yaml
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
site:
|
||||
title: Hello World
|
||||
|
||||
database:
|
||||
type: sqliteInvalid
|
||||
dsn: /path/to/db
|
||||
2
tests/fatal-syntax.evcc.yaml
Executable file
2
tests/fatal-syntax.evcc.yaml
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
s!ite:
|
||||
title: Hello World
|
||||
23
tests/fatal.spec.js
Normal file
23
tests/fatal.spec.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.describe("fatal", async () => {
|
||||
test("evcc yaml error", async ({ page }) => {
|
||||
const instance = await start("fatal-syntax.evcc.yaml");
|
||||
await page.goto("/");
|
||||
await expect(page.getByTestId("bottom-banner")).toBeVisible();
|
||||
await expect(page.getByTestId("bottom-banner")).toContainText("failed parsing config file");
|
||||
await expect(page.getByTestId("generalconfig-password")).not.toBeVisible();
|
||||
await stop(instance);
|
||||
});
|
||||
test("database error", async ({ page }) => {
|
||||
const instance = await start("fatal-db.evcc.yaml");
|
||||
await page.goto("/");
|
||||
await expect(page.getByTestId("bottom-banner")).toBeVisible();
|
||||
await expect(page.getByTestId("bottom-banner")).toContainText("invalid database");
|
||||
await expect(page.getByTestId("generalconfig-password")).not.toBeVisible();
|
||||
await stop(instance);
|
||||
});
|
||||
});
|
||||
|
|
@ -13,6 +13,7 @@ test.afterAll(async () => {
|
|||
async function login(page) {
|
||||
await page.locator("#loginPassword").fill("secret");
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
await expect(page.locator("#loginPassword")).not.toBeVisible();
|
||||
}
|
||||
|
||||
test.describe("opening logs", async () => {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ test.use({ baseURL: baseUrl() });
|
|||
async function login(page) {
|
||||
await page.locator("#loginPassword").fill("secret");
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
await expect(page.locator("#loginPassword")).not.toBeVisible();
|
||||
}
|
||||
|
||||
test.describe("Basics", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue