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