Repository: kestra-io/kestra (java)

Issue:
Provides a better error message when Kestra cannot be started due to misconfiguration
### Feature description

When Kestra cannot be started due to misconfiguration, en error like this one is displayed on the log:

```
D:\code\kestra> .\kestra-0.17.7.bat server standalone
Exception in thread "main" io.micronaut.context.exceptions.BeanInstantiationException: Bean definition [io.kestra.webserver.services.BasicAuthService] could not be loaded: Failed to inject value for field [settingRepository] of class: io.kestra.webserver.services.BasicAuthService

Message: No bean of type [io.kestra.core.repositories.SettingRepositoryInterface] exists.
* [H2SettingRepository] a candidate of [SettingRepositoryInterface] is disabled because:
 - Required property [kestra.repository.type] with value [h2] not present
* [MysqlSettingRepository] a candidate of [SettingRepositoryInterface] is disabled because:
 - Required property [kestra.repository.type] with value [mysql] not present
* [PostgresSettingRepository] a candidate of [SettingRepositoryInterface] is disabled because:
 - Required property [kestra.repository.type] with value [postgres] not present
Path Taken: new BasicAuthService() --> BasicAuthService.settingRepository
        at io.micronaut.context.DefaultBeanContext.initializeContext(DefaultBeanContext.java:1982)
        at io.micronaut.context.DefaultApplicationContext.initializeContext(DefaultApplicationContext.java:294)
        at io.micronaut.context.DefaultBeanContext.readAllBeanDefinitionClasses(DefaultBeanContext.java:3348)
        at io.micronaut.context.DefaultBeanContext.finalizeConfiguration(DefaultBeanContext.java:3692)
        at io.micronaut.context.DefaultBeanContext.start(DefaultBeanContext.java:342)
        at io.micronaut.context.DefaultApplicationContext.start(DefaultApplicationContext.java:202)
        at io.micronaut.configuration.picocli.MicronautFactory.<init>(MicronautFactory.java:59)
        at io.kestra.cli.App.execute(App.java:67)
        at io.kestra.cli.App.main(App.java:50)
Caused by: io.micronaut.context.exceptions.DependencyInjectionException: Failed to inject value for field [settingRepository] of class: io.kestra.webserver.services.BasicAuthService

Message: No bean of type [io.kestra.core.repositories.SettingRepositoryInterface] exists.
* [H2SettingRepository] a candidate of [SettingRepositoryInterface] is disabled because:
 - Required property [kestra.repository.type] with value [h2] not present
* [MysqlSettingRepository] a candidate of [SettingRepositoryInterface] is disabled because:
 - Required property [kestra.repository.type] with value [mysql] not present
* [PostgresSettingRepository] a candidate of [SettingRepositoryInterface] is disabled because:
 - Required property [kestra.repository.type] with value [postgres] not present
Path Taken: new BasicAuthService() --> BasicAuthService.settingRepository
        at io.micronaut.context.AbstractInitializableBeanDefinition.resolveBean(AbstractInitializableBeanDefinition.java:2177)
        at io.micronaut.context.AbstractInitializableBeanDefinition.getBeanForField(AbstractInitializableBeanDefinition.java:1668)
        at io.kestra.webserver.services.$BasicAuthService$Definition.inject(Unknown Source)
        at io.kestra.webserver.services.$BasicAuthService$Definition.instantiate(Unknown Source)
        at io.micronaut.context.DefaultBeanContext.resolveByBeanFactory(DefaultBeanContext.java:2311)
        at io.micronaut.context.DefaultBeanContext.doCreateBean(DefaultBeanContext.java:2281)
        at io.micronaut.context.DefaultBeanContext.doCreateBean(DefaultBeanContext.java:2293)
        at io.micronaut.context.DefaultBeanContext.createRegistration(DefaultBeanContext.java:3095)
        at io.micronaut.context.SingletonScope.getOrCreate(SingletonScope.java:80)
        at io.micronaut.context.DefaultBeanContext.findOrCreateSingletonBeanRegistration(DefaultBeanContext.java:2997)
        at io.micronaut.context.DefaultBeanContext.initializeEagerBean(DefaultBeanContext.java:2678)
        at io.micronaut.context.DefaultBeanContext.initializeContext(DefaultBeanContext.java:1976)
        ... 8 more
Caused by: io.micronaut.context.exceptions.NoSuchBeanException: No bean of type [io.kestra.core.repositories.SettingRepositoryInterface] exists.
* [H2SettingRepository] a candidate of [SettingRepositoryInterface] is disabled because:
 - Required property [kestra.repository.type] with value [h2] not present
* [MysqlSettingRepository] a candidate of [SettingRepositoryInterface] is disabled because:
 - Required property [kestra.repository.type] with value [mysql] not present
* [PostgresSettingRepository] a candidate of [SettingRepositoryInterface] is disabled because:
 - Required property [kestra.repository.type] with value [postgres] not present
        at io.micronaut.context.DefaultBeanContext.newNoSuchBeanException(DefaultBeanContext.java:2768)
        at io.micronaut.context.DefaultApplicationContext.newNoSuchBeanException(DefaultApplicationContext.java:309)
        at io.micronaut.context.DefaultBeanContext.resolveBeanRegistration(DefaultBeanContext.java:2737)
        at io.micronaut.context.DefaultBeanContext.getBean(DefaultBeanContext.java:1731)
        at io.micronaut.context.AbstractBeanResolutionContext.getBean(AbstractBeanResolutionContext.java:89)
        at io.micronaut.context.AbstractInitializableBeanDefinition.resolveBean(AbstractInitializableBeanDefinition.java:2161)
        ... 19 more
```

A user not knowing how Kestra work has no way to understand what's going on, this is usually due to missing configuration so we should detect this and points to the correct configuration guide.

We should check preconditions on all Kestra server commands and display a nice error message for the common cause of misconfiguration that is missing queue, repository or storage configuration.

Pull request description:
feat(cli): descriptive server start error via bean
### What changes are being made and why?

This is a new iteration of #4171 attempting to resolve #4155 properly. The `MICRONAUT_ENVIRONMENTS` env variable is no longer ignored.

The validation is performed via a `@Context`-scoped bean. I'm no Micronaut DI expert 😐

Patch:
diff --git a/.plugins b/.plugins
index fdbe2dfbf..b64a00e46 100644
--- a/.plugins
+++ b/.plugins
@@ -31,6 +31,7 @@
 #io.kestra.plugin:plugin-github:LATEST
 #io.kestra.plugin:plugin-googleworkspace:LATEST
 #io.kestra.plugin:plugin-hightouch:LATEST
+#io.kestra.plugin:plugin-hubspot:LATEST
 #io.kestra.plugin:plugin-jdbc-as400:LATEST
 #io.kestra.plugin:plugin-jdbc-clickhouse:LATEST
 #io.kestra.plugin:plugin-jdbc-db2:LATEST
@@ -89,6 +90,7 @@
 #io.kestra.plugin:plugin-terraform:LATEST
 #io.kestra.plugin:plugin-tika:LATEST
 #io.kestra.plugin:plugin-weaviate:LATEST
+#io.kestra.plugin:plugin-zendesk:LATEST
 #io.kestra.storage:storage-azure:LATEST
 #io.kestra.storage:storage-gcs:LATEST
 #io.kestra.storage:storage-minio:LATEST
diff --git a/cli/src/main/java/io/kestra/cli/ServerCommandValidator.java b/cli/src/main/java/io/kestra/cli/ServerCommandValidator.java
new file mode 100644
index 000000000..7501c73f0
--- /dev/null
+++ b/cli/src/main/java/io/kestra/cli/ServerCommandValidator.java
@@ -0,0 +1,55 @@
+package io.kestra.cli;
+
+import io.micronaut.context.annotation.Context;
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.context.env.Environment;
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Inject;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.Serial;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Context
+@Requires(property = "kestra.server-type")
+public class ServerCommandValidator {
+    private static final Map<String, String> VALIDATED_PROPERTIES = Map.of(
+        "kestra.queue.type", "https://kestra.io/docs/configuration-guide/setup#queue-configuration",
+        "kestra.repository.type", "https://kestra.io/docs/configuration-guide/setup#repository-configuration",
+        "kestra.storage.type", "https://kestra.io/docs/configuration-guide/setup#internal-storage-configuration"
+    );
+
+    private final Environment environment;
+
+    @Inject
+    public ServerCommandValidator(final Environment environment) {
+        this.environment = environment;
+    }
+
+    @PostConstruct
+    void validate() {
+        final List<Map.Entry<String, String>> missingProperties = VALIDATED_PROPERTIES.entrySet().stream()
+            .filter((property) -> !environment.containsProperty(property.getKey()))
+            .toList();
+
+        missingProperties.forEach(property -> log.error("""
+            Server configuration requires the '{}' property to be defined.
+            For more details, please follow the official setup guide at: {}""", property.getKey(), property.getValue())
+        );
+
+        if (!missingProperties.isEmpty()) {
+            throw new ServerCommandException("Incomplete server configuration - missing required properties");
+        }
+    }
+
+    public static class ServerCommandException extends RuntimeException {
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        public ServerCommandException(String errorMessage) {
+            super(errorMessage);
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/src/components/admin/stats/BasicAuthPrompt.vue b/ui/src/components/admin/stats/BasicAuthPrompt.vue
index 8515d940d..9557c2f9c 100644
--- a/ui/src/components/admin/stats/BasicAuthPrompt.vue
+++ b/ui/src/components/admin/stats/BasicAuthPrompt.vue
@@ -20,6 +20,7 @@
                 <el-form-item
                     :label="$t('password')"
                     required
+                    prop="password"
                 >
                     <el-input v-model="form.password" type="password" show-password />
                 </el-form-item>
@@ -62,6 +63,28 @@
                             trigger: ["blur"],
                             pattern: "^$|^[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"
                         },
+                        {
+                            validator: (rule, value, callback) => {
+                                if (value && value.length > 256) {
+                                    callback(new Error(this.$t("email length constraint")));
+                                } else {
+                                    callback();
+                                }
+                            },
+                            trigger: ["blur", "change"]
+                        }
+                    ],
+                    password: [
+                        {
+                            validator: (rule, value, callback) => {
+                                if (value && value.length > 256) {
+                                    callback(new Error(this.$t("password length constraint")));
+                                } else {
+                                    callback();
+                                }
+                            },
+                            trigger: ["blur", "change"]
+                        }
                     ],
                     confirmPassword: [
                         {
diff --git a/ui/src/components/executions/date-select/DateFilter.vue b/ui/src/components/executions/date-select/DateFilter.vue
index 5810f7bb3..10cbdcf43 100644
--- a/ui/src/components/executions/date-select/DateFilter.vue
+++ b/ui/src/components/executions/date-select/DateFilter.vue
@@ -4,10 +4,10 @@
         @change="onSelectedFilterType()"
         class="filter"
     >
-        <el-radio-button :value="filterType.RELATIVE">
+        <el-radio-button data-test-id="date-filter-relative-selector" :value="filterType.RELATIVE">
             {{ $t("relative") }}
         </el-radio-button>
-        <el-radio-button :value="filterType.ABSOLUTE">
+        <el-radio-button data-test-id="date-filter-absolute-selector" :value="filterType.ABSOLUTE">
             {{ $t("absolute") }}
         </el-radio-button>
     </el-radio-group>
diff --git a/ui/src/components/executions/date-select/DateSelect.vue b/ui/src/components/executions/date-select/DateSelect.vue
index 87e074773..682fef667 100644
--- a/ui/src/components/executions/date-select/DateSelect.vue
+++ b/ui/src/components/executions/date-select/DateSelect.vue
@@ -1,6 +1,7 @@
 <template>
     <el-tooltip :disabled="tooltip === undefined" :content="tooltip" effect="light">
         <el-select
+            data-test-id="time-selector"
             :model-value="value"
             :placeholder="placeholder"
             @change="$emit('change', $event)"
diff --git a/ui/src/components/layout/RefreshButton.vue b/ui/src/components/layout/RefreshButton.vue
index 265539a37..607bc26a4 100644
--- a/ui/src/components/layout/RefreshButton.vue
+++ b/ui/src/components/layout/RefreshButton.vue
@@ -1,11 +1,11 @@
 <template>
     <el-button-group data-component="FILENAME_PLACEHOLDER" :size="size" :class="customClass">
-        <el-button :disabled="!canAutoRefresh" :active="autoRefresh" @click="toggleAutoRefresh">
+        <el-button :disabled="!canAutoRefresh" :active="autoRefresh" @click="toggleAutoRefresh" data-test-id="toggle-aut-refresh-button">
             <kicon :tooltip="$t('toggle periodic refresh each 10 seconds')" placement="bottom">
                 <component :is="autoRefresh ? 'auto-renew' : 'auto-renew-off'" class="auto-refresh-icon" />
             </kicon>
         </el-button>
-        <el-button @click="triggerRefresh">
+        <el-button @click="triggerRefresh" data-test-id="trigger-refresh-button">
             <kicon :tooltip="$t('trigger refresh')" placement="bottom">
                 <refresh />
             </kicon>
diff --git a/ui/src/components/logs/LogsWrapper.vue b/ui/src/components/logs/LogsWrapper.vue
index 33a251499..a83f9ec85 100644
--- a/ui/src/components/logs/LogsWrapper.vue
+++ b/ui/src/components/logs/LogsWrapper.vue
@@ -9,6 +9,7 @@
                     </el-form-item>
                     <el-form-item>
                         <namespace-select
+                            data-test-id="logs-namespace-selector"
                             data-type="flow"
                             v-if="$route.name !== 'flows/update'"
                             :value="$route.query.namespace"
@@ -17,6 +18,7 @@
                     </el-form-item>
                     <el-form-item>
                         <log-level-selector
+                            data-test-id="logs-log-level-selector"
                             :value="selectedLogLevel"
                             @update:model-value="onDataTableValue('level', $event)"
                         />
diff --git a/ui/src/components/namespace/Namespace.vue b/ui/src/components/namespace/Namespace.vue
index 606e18b8c..3e024e20b 100644
--- a/ui/src/components/namespace/Namespace.vue
+++ b/ui/src/components/namespace/Namespace.vue
@@ -60,11 +60,11 @@
             ...mapState("auth", ["user"]),
             ...mapState("namespace", ["dependencies"]),
             canCreateSecret() {
-                return this.$route.params.namespace && this.user &&
-                    this.user.isAllowed(permission.SECRET, action.CREATE, this.$route.params.namespace);
+                return this.$route.params.id && this.user &&
+                    this.user.isAllowed(permission.SECRET, action.CREATE, this.$route.params.id);
             },
             canCreateKv() {
-                return this.$route.params.namespace;
+                return this.$route.params.id;
             },
             routeInfo() {
                 return {
@@ -217,12 +217,8 @@
         },
         methods: {
             loadItem() {
-                if (this.$route.params.namespace) {
-                    this.$store
-                        .dispatch(
-                            "namespace/load",
-                            this.$route.params
-                        )
+                if (this.$route.params.id) {
+                    this.$store.dispatch("namespace/load",this.$route.params.id)
                 }
             },
         }
diff --git a/ui/src/components/namespace/NamespaceDependenciesWrapper.vue b/ui/src/components/namespace/NamespaceDependenciesWrapper.vue
index 4a17a2b5d..e55bd2749 100644
--- a/ui/src/components/namespace/NamespaceDependenciesWrapper.vue
+++ b/ui/src/components/namespace/NamespaceDependenciesWrapper.vue
@@ -1,5 +1,5 @@
 <template>
-    <NamespaceDependencies :id="route.params.namespace" />
+    <NamespaceDependencies :id="route.params.id" />
 </template>
 
 <script setup>
diff --git a/ui/src/components/namespace/NamespaceFlows.vue b/ui/src/components/namespace/NamespaceFlows.vue
index 65ced1dc6..d8cd73346 100644
--- a/ui/src/components/namespace/NamespaceFlows.vue
+++ b/ui/src/components/namespace/NamespaceFlows.vue
@@ -1,5 +1,5 @@
 <template>
-    <data-table :total="total">
+    <data-table @page-changed="onPageChanged" :size="pageSize" :page="pageNumber" :total="total">
         <template #table>
             <el-table
                 :data="flows"
@@ -111,6 +111,7 @@
     import StateChart from "../stats/StateChart.vue";
     import TriggerAvatar from "../flows/TriggerAvatar.vue";
     import Kicon from "../Kicon.vue"
+    import _merge from "lodash/merge";
 
     import TextSearch from "vue-material-design-icons/TextSearch.vue";
 
@@ -128,8 +129,17 @@
             }
         },
         methods: {
+            loadQuery(base) {               
+                return _merge(base, this.queryWithFilter())
+            },
             loadData(callback) {
-                this.$store.dispatch("flow/findFlows", {namespace: this.$route.params.namespace}).then((flows) => {
+                const params =  {
+                    namespace: this.$route.params.id,
+                    page: this.$route.query.page || this.internalPageNumber, 
+                    size: this.$route.query.size || this.internalPageSize
+                }
+
+                this.$store.dispatch("flow/findFlows", this.loadQuery(params)).then((flows) => {
                     this.dailyGroupByFlowReady = false;
                     this.lastExecutionByFlowReady = false;
 
diff --git a/ui/src/components/namespace/NamespaceKV.vue b/ui/src/components/namespace/NamespaceKV.vue
index dd2f05ac3..9c3022a5e 100644
--- a/ui/src/components/namespace/NamespaceKV.vue
+++ b/ui/src/components/namespace/NamespaceKV.vue
@@ -139,10 +139,10 @@
                 return this.kv.key ? this.$t("kv.update", {key: this.kv.key}) : this.$t("kv.add");
             },
             canUpdateKv() {
-                return this.$route.params.namespace                
+                return this.$route.params.id                
             },
             canDeleteKv() {
-                return this.$route.params.namespace
+                return this.$route.params.id
             },
             addKvDrawerVisible: {
                 get() {
@@ -216,8 +216,7 @@
                 }
             },
             loadKvs() {
-                this.$store
-                    .dispatch("namespace/kvsList", {id: this.$route.params.namespace})
+                this.$store.dispatch("namespace/kvsList", {id: this.$route.params.id})
             },
             kvKeyDuplicate(rule, value, callback) {
                 if (this.kv.update === undefined && this.kvs && this.kvs.find(r => r.key === value)) {
@@ -228,7 +227,7 @@
             },
             async updateKvModal(key) {
                 this.kv.key = key;
-                const {type, value} = await this.$store.dispatch("namespace/kv", {namespace: this.$route.params.namespace, key});
+                const {type, value} = await this.$store.dispatch("namespace/kv", {namespace: this.$route.params.id, key});
                 this.kv.type = type;
                 if (type === "JSON") {
                     this.kv.value = JSON.stringify(value);
@@ -243,7 +242,7 @@
             removeKv(key) {
                 this.$toast().confirm(this.$t("delete confirm", {name: key}), () => {
                     return this.$store
-                        .dispatch("namespace/deleteKv", {namespace: this.$route.params.namespace, key: key})
+                        .dispatch("namespace/deleteKv", {namespace: this.$route.params.id, key: key})
                         .then(() => {
                             this.$toast().deleted(key);
                         });
@@ -270,7 +269,7 @@
                     return this.$store
                         .dispatch(
                             "namespace/createKv",
-                            {namespace: this.$route.params.namespace, ...this.kv, value}
+                            {namespace: this.$route.params.id, ...this.kv, value}
                         )
                         .then(() => {
                             this.$toast().saved(this.kv.key);
diff --git a/ui/src/components/namespace/Overview.vue b/ui/src/components/namespace/Overview.vue
index 3d9b66c31..360c3adb4 100644
--- a/ui/src/components/namespace/Overview.vue
+++ b/ui/src/components/namespace/Overview.vue
@@ -1,9 +1,9 @@
 <template>
     <div>
         <home
-            :namespace="$route.params.namespace || $route.query.id"
+            :namespace="$route.params.id || $route.query.id"
             :restore-url="false"
-            :description="$route.params.namespace ? namespace?.description : undefined"
+            :description="$route.params.id ? namespace?.description : undefined"
             embed
         />
     </div>
diff --git a/ui/src/components/saved-filters/Filters.vue b/ui/src/components/saved-filters/Filters.vue
index 76ac8947d..697dead35 100644
--- a/ui/src/components/saved-filters/Filters.vue
+++ b/ui/src/components/saved-filters/Filters.vue
@@ -1,11 +1,11 @@
 <template>
     <!-- No filter yet -->
-    <el-button v-if="!hasSavedFilters" :icon="ContentSave" @click="toggleDrawer()">
+    <el-button v-if="!hasSavedFilters" data-test-id="no-filters-selector" :icon="ContentSave" @click="toggleDrawer()">
         {{ $t("search filters.filters") }}
     </el-button>
     <!-- Existing filters -->
     <el-dropdown v-else button type="default" popper-class="disabled-means-selected">
-        <el-button class="dropdown-button" :icon="ContentSave" @click="toggleDrawer()">
+        <el-button class="dropdown-button" data-test-id="filters-selector" :icon="ContentSave" @click="toggleDrawer()">
             {{ $t("search filters.filters") }}
         </el-button>
         <template #dropdown>
diff --git a/ui/src/stores/namespaces.js b/ui/src/stores/namespaces.js
index 38b76fb1f..e6217496f 100644
--- a/ui/src/stores/namespaces.js
+++ b/ui/src/stores/namespaces.js
@@ -27,8 +27,8 @@ export default {
                     return response.data.results;
                 })
         },
-        load({commit}, options) {
-            return this.$http.get(`${apiUrl(this)}/namespaces/${options.id}`, VALIDATE)
+        load({commit}, id) {
+            return this.$http.get(`${apiUrl(this)}/namespaces/${id}`, VALIDATE)
                 .then(response => {
                     if(response.status === 200) commit("setNamespace", response.data)
                     return response.data;
diff --git a/ui/src/translations/en.json b/ui/src/translations/en.json
index 2708cb4b6..3396bd62f 100644
--- a/ui/src/translations/en.json
+++ b/ui/src/translations/en.json
@@ -251,6 +251,8 @@
     "email": "Email",
     "password": "Password",
     "confirm password": "Confirm password",
+    "email length constraint": "Email must not exceed 256 characters",
+    "password length constraint": "Password must not exceed 256 characters",
     "passwords do not match": "Passwords do not match",
     "avg duration": "Avg. execution duration",
     "neutral trend": "Stable",
diff --git a/ui/src/translations/fr.json b/ui/src/translations/fr.json
index 3717f0bdf..2a1985d15 100644
--- a/ui/src/translations/fr.json
+++ b/ui/src/translations/fr.json
@@ -244,6 +244,8 @@
     "email": "Email",
     "password": "Mot de passe",
     "confirm password": "Confirmer le mot de passe",
+    "email length constraint": "L'e-mail ne doit pas dépasser 256 caractères.",
+    "password length constraint": "Le mot de passe ne doit pas dépasser 256 caractères.",
     "passwords do not match": "Les mots de passe ne correspondent pas",
     "avg duration": "Durée moyenne d'exécution",
     "neutral trend": "Stable",
diff --git a/webserver/src/main/java/io/kestra/webserver/services/BasicAuthService.java b/webserver/src/main/java/io/kestra/webserver/services/BasicAuthService.java
index 9fa03b063..84fefc650 100644
--- a/webserver/src/main/java/io/kestra/webserver/services/BasicAuthService.java
+++ b/webserver/src/main/java/io/kestra/webserver/services/BasicAuthService.java
@@ -31,6 +31,7 @@ import java.util.regex.Pattern;
 public class BasicAuthService {
     public static final String BASIC_AUTH_SETTINGS_KEY = "kestra.server.basic-auth";
     private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$");
+    private static final int EMAIL_PASSWORD_MAX_LEN = 256;
 
     @Inject
     private SettingRepositoryInterface settingRepository;
@@ -75,6 +76,12 @@ public class BasicAuthService {
             throw new IllegalArgumentException("No password set for Basic Authentication. Please provide a password.");
         }
 
+        if (basicAuthConfiguration.getUsername().length() > EMAIL_PASSWORD_MAX_LEN ||
+            basicAuthConfiguration.password.length() > EMAIL_PASSWORD_MAX_LEN) {
+            throw new IllegalArgumentException("The length of email or password should not exceed 256 characters.");
+        }
+
+
         SaltedBasicAuthConfiguration previousConfiguration = this.configuration();
         String salt = previousConfiguration == null
             ? null


Test patch:
diff --git a/cli/src/test/java/io/kestra/cli/ServerCommandValidatorTest.java b/cli/src/test/java/io/kestra/cli/ServerCommandValidatorTest.java
new file mode 100644
index 000000000..00ef51184
--- /dev/null
+++ b/cli/src/test/java/io/kestra/cli/ServerCommandValidatorTest.java
@@ -0,0 +1,53 @@
+package io.kestra.cli;
+
+import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.exceptions.BeanInstantiationException;
+import io.micronaut.context.exceptions.NoSuchBeanException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class ServerCommandValidatorTest {
+
+    @Test
+    void noServerCommandIssued() {
+        try (ApplicationContext context = ApplicationContext.run()) {
+            Assertions.assertThrows(NoSuchBeanException.class, () -> context.getBean(ServerCommandValidator.class));
+        }
+    }
+
+    @Test
+    void serverCommandIssued() {
+        Assertions.assertDoesNotThrow(() -> ApplicationContext.builder()
+            .deduceEnvironment(false)
+            .properties(Map.of(
+                "kestra.server-type", "webserver",
+                "kestra.queue.type", "memory",
+                "kestra.repository.type", "memory",
+                "kestra.storage.type", "local"
+            ))
+            .start()
+        );
+
+        final Throwable exception = Assertions.assertThrows(BeanInstantiationException.class, () ->
+            ApplicationContext.builder()
+                .deduceEnvironment(false)
+                .properties(Map.of("kestra.server-type", "webserver"))
+                .start()
+        );
+        final Throwable rootException = getRootException(exception);
+        assertThat(rootException.getClass(), is(ServerCommandValidator.ServerCommandException.class));
+        assertThat(rootException.getMessage(), is("Incomplete server configuration - missing required properties"));
+    }
+
+    private Throwable getRootException(Throwable exception) {
+        while (exception.getCause() != null) {
+            exception = exception.getCause();
+        }
+        return exception;
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/java/io/kestra/plugin/core/http/DownloadTest.java b/core/src/test/java/io/kestra/plugin/core/http/DownloadTest.java
index 96edcd51d..5442df59b 100644
--- a/core/src/test/java/io/kestra/plugin/core/http/DownloadTest.java
+++ b/core/src/test/java/io/kestra/plugin/core/http/DownloadTest.java
@@ -30,7 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 
 @KestraTest
 class DownloadTest {
-    public static final String FILE = "http://speedtest.ftp.otenet.gr/files/test1Mb.db";
+    public static final String FILE = "https://sampletestfile.com/wp-content/uploads/2023/07/500KB-CSV.csv";
     @Inject
     private RunContextFactory runContextFactory;
 
@@ -56,11 +56,11 @@ class DownloadTest {
             IOUtils.toString(this.storageInterface.get(null, output.getUri()), StandardCharsets.UTF_8),
             is(IOUtils.toString(new URI(FILE).toURL().openStream(), StandardCharsets.UTF_8))
         );
-        assertThat(output.getUri().toString(), endsWith(".db"));
+        assertThat(output.getUri().toString(), endsWith(".csv"));
     }
 
     @Test
-    void noResponse() throws Exception {
+    void noResponse() {
         EmbeddedServer embeddedServer = applicationContext.getBean(EmbeddedServer.class);
         embeddedServer.start();
 
@@ -100,7 +100,7 @@ class DownloadTest {
     }
 
     @Test
-    void error() throws Exception {
+    void error() {
         EmbeddedServer embeddedServer = applicationContext.getBean(EmbeddedServer.class);
         embeddedServer.start();
 
diff --git a/core/src/test/java/io/kestra/plugin/core/http/RequestTest.java b/core/src/test/java/io/kestra/plugin/core/http/RequestTest.java
index db6257ac0..54caa12a1 100644
--- a/core/src/test/java/io/kestra/plugin/core/http/RequestTest.java
+++ b/core/src/test/java/io/kestra/plugin/core/http/RequestTest.java
@@ -11,6 +11,7 @@ import io.micronaut.context.ApplicationContext;
 import io.micronaut.http.*;
 import io.micronaut.http.annotation.Controller;
 import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.Head;
 import io.micronaut.http.annotation.Post;
 import io.micronaut.http.multipart.StreamingFileUpload;
 import io.micronaut.runtime.server.EmbeddedServer;
@@ -67,7 +68,7 @@ class RequestTest {
 
     @Test
     void head() throws Exception {
-        final String url = "http://speedtest.ftp.otenet.gr/files/test100Mb.db";
+        final String url = "https://sampletestfile.com/wp-content/uploads/2023/07/500KB-CSV.csv";
 
         Request task = Request.builder()
             .id(RequestTest.class.getSimpleName())
@@ -81,7 +82,7 @@ class RequestTest {
         Request.Output output = task.run(runContext);
 
         assertThat(output.getUri(), is(URI.create(url)));
-        assertThat(output.getHeaders().get("Content-Length").getFirst(), is("104857600"));
+        assertThat(output.getHeaders().get("content-length").getFirst(), is("512789"));
     }
 
     @Test
@@ -260,6 +261,11 @@ class RequestTest {
             return HttpResponse.ok("{ \"hello\": \"world\" }");
         }
 
+        @Head("/hello")
+        HttpResponse<String> head() {
+            return HttpResponse.ok();
+        }
+
         @Get("/hello417")
         HttpResponse<String> hello417() {
             return HttpResponse.status(HttpStatus.EXPECTATION_FAILED).body("{ \"hello\": \"world\" }");