Repository: hyperjumptech/monika
Pull request #1145 (2023-10-17 07:58:23)

Refactor: Replace Manual Validation with Joi
# Monika Pull Request (PR)

## What feature/issue does this PR add
Replace manual validation with [Joi]( [Joi](https://joi.dev/)) validation for probe data input.

## How did you implement / how did you fix it
1. Change manual validation with Joi validation.
2. Remove the following unknown properties from Symon.

- locations
- notifications
- projectId
- recoveryThreshold
- requests[0].alerts[0].createdAt
- requests[0].alerts[0].updatedAt
- requests[0].createdAt
- requests[0].ipAddress
- requests[0].order
- requests[0].port
- requests[0].probeId
- requests[0].protocol
- requests[0].updatedAt

## How to test
1. Run Monika with invalid configuration.
4. Run Monika with valid configuration.
5. Run Monika in Symon mode.

### Screenshot
#### Before
![image](https://github.com/hyperjumptech/monika/assets/15191978/54b71892-e499-4478-b528-c4ab75861843)

#### After
![image](https://github.com/hyperjumptech/monika/assets/15191978/c531059c-9a59-4fab-bd46-8eee5c97a2e4)

### Demo

Symon & Monika mode

https://github.com/hyperjumptech/monika/assets/15191978/e58145a2-0fbf-44d2-848e-fe98b1f4856a

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/hyperjumptech/monika/pull/1145?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) Report
> Merging [#1145](https://app.codecov.io/gh/hyperjumptech/monika/pull/1145?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) (27e8153) into [main](https://app.codecov.io/gh/hyperjumptech/monika/commit/a8b884c9ca8187838dbedec90e0221f53edd539a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) (a8b884c) will **decrease** coverage by `0.03%`.
> The diff coverage is `74.48%`.

> :exclamation: Current head 27e8153 differs from pull request most recent head 1924295. Consider uploading reports for the commit 1924295 to get more accurate results

```diff
@@            Coverage Diff             @@
##             main    #1145      +/-   ##
==========================================
- Coverage   59.81%   59.79%   -0.03%     
==========================================
  Files         111      105       -6     
  Lines        3240     3181      -59     
  Branches      641      623      -18     
==========================================
- Hits         1938     1902      -36     
+ Misses       1023     1005      -18     
+ Partials      279      274       -5     
```


| [Files](https://app.codecov.io/gh/hyperjumptech/monika/pull/1145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) | Coverage Δ | |
|---|---|---|
| [packages/notification/utils/http.ts](https://app.codecov.io/gh/hyperjumptech/monika/pull/1145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-cGFja2FnZXMvbm90aWZpY2F0aW9uL3V0aWxzL2h0dHAudHM=) | `76.92% <ø> (-1.65%)` | :arrow_down: |
| [src/components/config/index.ts](https://app.codecov.io/gh/hyperjumptech/monika/pull/1145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2NvbXBvbmVudHMvY29uZmlnL2luZGV4LnRz) | `69.91% <100.00%> (+0.24%)` | :arrow_up: |
| [src/components/config/parse-har.ts](https://app.codecov.io/gh/hyperjumptech/monika/pull/1145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2NvbXBvbmVudHMvY29uZmlnL3BhcnNlLWhhci50cw==) | `77.27% <ø> (ø)` | |
| [src/components/config/validate.ts](https://app.codecov.io/gh/hyperjumptech/monika/pull/1145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2NvbXBvbmVudHMvY29uZmlnL3ZhbGlkYXRlLnRz) | `77.77% <100.00%> (-4.05%)` | :arrow_down: |
| [src/plugins/validate-response/checkers/index.ts](https://app.codecov.io/gh/hyperjumptech/monika/pull/1145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL3BsdWdpbnMvdmFsaWRhdGUtcmVzcG9uc2UvY2hlY2tlcnMvaW5kZXgudHM=) | `100.00% <ø> (+21.42%)` | :arrow_up: |
| [src/symon/index.ts](https://app.codecov.io/gh/hyperjumptech/monika/pull/1145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL3N5bW9uL2luZGV4LnRz) | `64.74% <100.00%> (+0.69%)` | :arrow_up: |
| [src/utils/http.ts](https://app.codecov.io/gh/hyperjumptech/monika/pull/1145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL3V0aWxzL2h0dHAudHM=) | `100.00% <ø> (ø)` | |
| [src/components/config/get.ts](https://app.codecov.io/gh/hyperjumptech/monika/pull/1145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2NvbXBvbmVudHMvY29uZmlnL2dldC50cw==) | `75.00% <66.66%> (ø)` | |
| [...rc/components/config/validation/validator/probe.ts](https://app.codecov.io/gh/hyperjumptech/monika/pull/1145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2NvbXBvbmVudHMvY29uZmlnL3ZhbGlkYXRpb24vdmFsaWRhdG9yL3Byb2JlLnRz) | `69.62% <69.62%> (+1.97%)` | :arrow_up: |

... and [6 files with indirect coverage changes](https://app.codecov.io/gh/hyperjumptech/monika/pull/1145/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech)

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech)

kevinhermawan: Could you update `Joi` to the latest version too?

Patch:
diff --git a/.mocharc.json b/.mocharc.json
index d92ea3d9..31cd6186 100644
--- a/.mocharc.json
+++ b/.mocharc.json
@@ -2,5 +2,6 @@
   "require": ["ts-node/register"],
   "bail": true,
   "timeout": 5000,
+  "bail": true,
   "exit": true
 }
diff --git a/docs/src/pages/guides/probes.md b/docs/src/pages/guides/probes.md
index ead03ced..5a55976a 100644
--- a/docs/src/pages/guides/probes.md
+++ b/docs/src/pages/guides/probes.md
@@ -65,9 +65,8 @@ Details of the field are given in the table below.
 | headers (optional)           | Http headers you might need for your request.                                                                                                                                                                                                |
 | body (optional)              | Any http body if your method requires it.                                                                                                                                                                                                    |
 | interval (optional)          | Number of probe's interval (in seconds). Default value is 10 seconds.                                                                                                                                                                        |
-| alerts                       | The condition which will trigger an alert, and the subsequent notification method to send out the alert. See below for further details on alerts and notifications.                                                                          |
 | saveBody (optional)          | When set to true, the response body of the request is stored in the internal database. The default is off when not defined. This is to keep the log file size small as some responses can be sizable. The setting is for each probe request. |
-| alerts (optional)            | See [alerts](./alerts) section for detailed information.                                                                                                                                                                                     |
+| alerts (optional)            | The condition which will trigger an alert, and the subsequent notification method to send out the alert. See below for further details on alerts and notifications. See [alerts](./alerts) section for detailed information.                 |
 | ping (optional)              | (boolean), If set true then send a PING to the specified url instead.                                                                                                                                                                        |
 | allowUnauthorized (optional) | (boolean), If set to true, will make https agent to not check for ssl certificate validity                                                                                                                                                   |
 
diff --git a/packages/notification/utils/http.ts b/packages/notification/utils/http.ts
index 829b4adc..24d2ca62 100644
--- a/packages/notification/utils/http.ts
+++ b/packages/notification/utils/http.ts
@@ -32,19 +32,6 @@ const httpAgent = new http.Agent({ keepAlive: true })
 const httpsAgent = new https.Agent({ keepAlive: true })
 export const DEFAULT_TIMEOUT = 10_000
 
-export const HTTPMethods = new Set([
-  'DELETE',
-  'GET',
-  'HEAD',
-  'OPTIONS',
-  'PATCH',
-  'POST',
-  'PUT',
-  'PURGE',
-  'LINK',
-  'UNLINK',
-])
-
 // Create an instance of axios here so it will be reused instead of creating a new one all the time.
 const axiosInstance = axios.create()
 
diff --git a/src/components/config/get.ts b/src/components/config/get.ts
index 4f80f7e5..a1d1307b 100644
--- a/src/components/config/get.ts
+++ b/src/components/config/get.ts
@@ -98,18 +98,18 @@ async function parseConfigType(
   const parsed = await parseConfig(source, configType, flags)
 
   // ensure that the parsed config meets our formatting
-  await validateConfig(parsed)
+  const validatedConfig = await validateConfig(parsed)
 
   if (configType !== 'har') {
-    const isValidConfig = validateConfigWithSchema(parsed)
+    const isValidConfig = validateConfigWithSchema(validatedConfig)
     if (!isValidConfig.valid) {
       throw new Error(isValidConfig.message)
     }
   }
 
   return {
-    ...parsed,
-    probes: parsed.probes?.map((probe) => {
+    ...validatedConfig,
+    probes: validatedConfig.probes?.map((probe) => {
       const requests: RequestConfig[] | undefined = probe?.requests?.map(
         (request) => ({
           ...request,
diff --git a/src/components/config/index.ts b/src/components/config/index.ts
index f601b76a..113093f4 100644
--- a/src/components/config/index.ts
+++ b/src/components/config/index.ts
@@ -87,9 +87,10 @@ export const updateConfig = async (
   validate = true
 ): Promise<void> => {
   log.info('Updating config')
+  let validatedConfig = config
   if (validate) {
     try {
-      await validateConfig(config)
+      validatedConfig = await validateConfig(config)
     } catch (error: any) {
       if (isTestEnvironment) {
         // return error during tests
@@ -101,11 +102,11 @@ export const updateConfig = async (
     }
   }
 
-  const version = md5Hash(config)
+  const version = md5Hash(validatedConfig)
   const hasChangeConfig = getContext()?.config?.version !== version
 
   if (hasChangeConfig) {
-    const newConfig = addConfigVersion(config)
+    const newConfig = addConfigVersion(validatedConfig)
 
     setContext({ config: newConfig })
     emitter.emit(events.config.updated, newConfig)
@@ -116,9 +117,9 @@ export const updateConfig = async (
 export const setupConfig = async (flags: MonikaFlags): Promise<void> => {
   const validFlag = await createConfigIfEmpty(flags)
   const config = await getConfigFrom(validFlag)
-  await validateConfig(config)
+  const validatedConfig = await validateConfig(config)
 
-  setContext({ config: addConfigVersion(config) })
+  setContext({ config: addConfigVersion(validatedConfig) })
 
   watchConfigsChange(validFlag)
 }
diff --git a/src/components/config/parse-har.ts b/src/components/config/parse-har.ts
index 12d876bb..3534664c 100644
--- a/src/components/config/parse-har.ts
+++ b/src/components/config/parse-har.ts
@@ -71,6 +71,7 @@ export const parseHarFile = (fileContents: string): Config => {
     const harConfig: any = {
       probes: [
         {
+          id: harRequest[0].url,
           requests: harRequest,
         },
       ],
diff --git a/src/components/config/validate.ts b/src/components/config/validate.ts
index 0e802c3d..3efdef3d 100644
--- a/src/components/config/validate.ts
+++ b/src/components/config/validate.ts
@@ -22,24 +22,24 @@
  * SOFTWARE.                                                                      *
  **********************************************************************************/
 
-import { Config } from '../../interfaces/config'
+import type { Config } from '../../interfaces/config'
 import { validateProbes, validateSymonConfig } from './validation'
 import { validateNotification } from '@hyperjumptech/monika-notification'
 
 export const validateConfig = async (
   configuration: Partial<Config>
-): Promise<void> => {
+): Promise<Config> => {
   const { notifications = [], probes = [], symon } = configuration
   const symonConfigError = validateSymonConfig(symon)
-  const validateProbesError = validateProbes(probes)
-
-  await validateNotification(notifications)
-
-  if (validateProbesError) {
-    throw new Error(validateProbesError)
-  }
 
   if (symonConfigError) {
     throw new Error(`Monika configuration: symon ${symonConfigError}`)
   }
+
+  const [validatedProbes] = await Promise.all([
+    validateProbes(probes),
+    validateNotification(notifications),
+  ])
+
+  return { ...configuration, probes: validatedProbes }
 }
diff --git a/src/components/config/validation/validator/alert.ts b/src/components/config/validation/validator/alert.ts
deleted file mode 100644
index 3b3270b7..00000000
--- a/src/components/config/validation/validator/alert.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-/**********************************************************************************
- * MIT License                                                                    *
- *                                                                                *
- * Copyright (c) 2021 Hyperjump Technology                                        *
- *                                                                                *
- * Permission is hereby granted, free of charge, to any person obtaining a copy   *
- * of this software and associated documentation files (the "Software"), to deal  *
- * in the Software without restriction, including without limitation the rights   *
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell      *
- * copies of the Software, and to permit persons to whom the Software is          *
- * furnished to do so, subject to the following conditions:                       *
- *                                                                                *
- * The above copyright notice and this permission notice shall be included in all *
- * copies or substantial portions of the Software.                                *
- *                                                                                *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR     *
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,       *
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE    *
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER         *
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,  *
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE  *
- * SOFTWARE.                                                                      *
- **********************************************************************************/
-
-import { Probe, ProbeAlert } from '../../../../interfaces/probe'
-import { parseAlertStringTime } from '../../../../plugins/validate-response/checkers'
-import { compileExpression } from '../../../../utils/expression-parser'
-
-const alertStatusMessage = 'status-not-2xx'
-const responseTimePrefix = 'response-time-greater-than-'
-
-const isValidProbeAlert = (alert: ProbeAlert | string): boolean => {
-  try {
-    if (typeof alert === 'string') {
-      return (
-        alert === alertStatusMessage ||
-        (alert.startsWith(responseTimePrefix) &&
-          Boolean(parseAlertStringTime(alert)))
-      )
-    }
-
-    return Boolean(
-      compileExpression(alert.assertion || (alert.query as string))
-    )
-  } catch {
-    return false
-  }
-}
-
-const convertOldAlertToNewFormat = (
-  probe: Probe,
-  allAlerts: ProbeAlert[]
-): void => {
-  probe.alerts = allAlerts.map((alert: any) => {
-    if (typeof alert === 'string') {
-      let query = ''
-      let message = ''
-      const subject = ''
-
-      if (alert === alertStatusMessage) {
-        query = 'response.status < 200 or response.status > 299'
-        message = 'HTTP Status is {{ response.status }}, expecting 200'
-      } else if (alert.startsWith(responseTimePrefix)) {
-        const expectedTime = parseAlertStringTime(alert)
-        query = `response.time > ${expectedTime}`
-        message = `Response time is {{ response.time }}ms, expecting less than ${expectedTime}ms`
-      }
-
-      return { query, subject, message }
-    }
-
-    return alert
-  })
-}
-
-export const validateAlerts = (probe: Probe): string | undefined => {
-  const { alerts = [], socket } = probe
-  const socketAlerts = socket?.alerts ?? []
-  const allAlerts = [...alerts, ...socketAlerts]
-
-  // Check probe alert properties
-  for (const alert of allAlerts) {
-    const check = isValidProbeAlert(alert)
-    if (!check) {
-      return `Probe alert format is invalid! (${alert})`
-    }
-  }
-
-  convertOldAlertToNewFormat(probe, allAlerts)
-}
diff --git a/src/components/config/validation/validator/mongo-config.ts b/src/components/config/validation/validator/mongo-config.ts
deleted file mode 100644
index 80b15cd3..00000000
--- a/src/components/config/validation/validator/mongo-config.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/**********************************************************************************
- * MIT License                                                                    *
- *                                                                                *
- * Copyright (c) 2021 Hyperjump Technology                                        *
- *                                                                                *
- * Permission is hereby granted, free of charge, to any person obtaining a copy   *
- * of this software and associated documentation files (the "Software"), to deal  *
- * in the Software without restriction, including without limitation the rights   *
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell      *
- * copies of the Software, and to permit persons to whom the Software is          *
- * furnished to do so, subject to the following conditions:                       *
- *                                                                                *
- * The above copyright notice and this permission notice shall be included in all *
- * copies or substantial portions of the Software.                                *
- *                                                                                *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR     *
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,       *
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE    *
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER         *
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,  *
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE  *
- * SOFTWARE.                                                                      *
- **********************************************************************************/
-
-import Joi from 'joi'
-import { Mongo } from '../../../../interfaces/probe'
-
-export const validateMongoConfig = (
-  mongoConfig?: Mongo[]
-): string | undefined => {
-  if (!mongoConfig) {
-    return ''
-  }
-
-  const schema = Joi.alternatives([
-    Joi.object({
-      uri: Joi.string(),
-    }),
-    Joi.object({
-      host: Joi.alternatives().try(Joi.string().hostname(), Joi.string().ip()),
-      port: Joi.number().min(0).max(65_536).required(),
-      password: Joi.string(),
-      username: Joi.string(),
-    }),
-  ])
-
-  for (const mongo of mongoConfig) {
-    const validationError = schema.validate(mongo)
-    if (validationError?.error?.message) return validationError?.error?.message
-  }
-}
diff --git a/src/components/config/validation/validator/probe.ts b/src/components/config/validation/validator/probe.ts
index acde0805..9aaf423b 100644
--- a/src/components/config/validation/validator/probe.ts
+++ b/src/components/config/validation/validator/probe.ts
@@ -22,59 +22,340 @@
  * SOFTWARE.                                                                      *
  **********************************************************************************/
 
-import { Probe } from '../../../../interfaces/probe'
-import { validateAlerts } from './alert'
-import { validateRequests } from './request'
-import { validateSchemaConfig } from './schema-config'
-
-const NO_PROBES = 'Probes object does not exists or has length lower than 1!'
-const PROBE_NO_REQUESTS =
-  'Probe requests does not exists or has length lower than 1!'
-
-const checkTotalProbes = (probe: Probe): string | undefined => {
-  const { requests, socket, redis, mongo, postgres, mariadb, mysql, ping } =
-    probe
-
-  const totalProbes =
-    (socket ? 1 : 0) +
-    (redis ? 1 : 0) +
-    (mongo ? 1 : 0) +
-    (postgres ? 1 : 0) +
-    (mariadb ? 1 : 0) +
-    (mysql ? 1 : 0) +
-    (ping ? 1 : 0) +
-    (requests?.length ?? 0)
-  if (totalProbes === 0) return PROBE_NO_REQUESTS
-}
+import joi from 'joi'
+import { v4 as uuid } from 'uuid'
+import type { Probe, ProbeAlert } from '../../../../interfaces/probe'
+import { isSymonModeFrom } from '../..'
+import { getContext } from '../../../../context'
+import { compileExpression } from '../../../../utils/expression-parser'
+import { isValidURL } from '../../../../utils/is-valid-url'
 
-export const validateProbes = (probes: Probe[]): string | undefined => {
-  if (probes.length === 0) return NO_PROBES
+export async function validateProbes(probes: Probe[]): Promise<Probe[]> {
+  const alertSchema = joi.alternatives().try(
+    joi.string(),
+    joi.object({
+      assertion: joi.string().allow(''),
+      id: joi.string().allow(''),
+      message: joi.string().allow(''),
+      query: joi.string().allow(''),
+    })
+  )
+  const mysqlSchema = joi.object({
+    command: joi.string().allow(''),
+    data: joi.alternatives().try(joi.string().allow(''), joi.number()),
+    database: joi.string().allow(''),
+    host: joi
+      .alternatives()
+      .try(joi.string().allow('').hostname(), joi.string().allow('').ip())
+      .required(),
+    password: joi.string().allow(''),
+    port: joi.number().default(3306),
+    username: joi.string().allow(''),
+  })
+  const schema = joi
+    .array<Probe[]>()
+    .min(isSymonModeFrom(getContext().flags) ? 0 : 1)
+    .messages({
+      'array.min': 'Probes object does not exists or has length lower than 1!',
+    })
+    .items(
+      joi.object({
+        alerts: joi.array().items(alertSchema),
+        description: joi.string().allow(''),
+        id: joi.string().required(),
+        incidentThreshold: joi.number().default(5).min(1),
+        interval: joi.number().default(10).min(1),
+        lastEvent: joi.object({
+          createdAt: joi.string().allow(''),
+          recoveredAt: joi.string().allow('', null),
+        }),
+        name: joi.string().allow(''),
+        mariadb: joi.array().items(mysqlSchema),
+        mongo: joi.array().items(
+          joi.alternatives([
+            joi.object({
+              alerts: joi.array().items(alertSchema),
+              uri: joi.string().required(),
+            }),
+            joi.object({
+              alerts: joi.array().items(alertSchema),
+              host: joi
+                .alternatives()
+                .try(
+                  joi.string().allow('').hostname(),
+                  joi.string().allow('').ip()
+                )
+                .required(),
+              password: joi.string().allow(''),
+              port: joi.number().default(27_017).min(0).max(65_536),
+              username: joi.string().allow(''),
+            }),
+          ])
+        ),
+        mysql: joi.array().items(mysqlSchema),
+        ping: joi.array().items(
+          joi.object({
+            alerts: joi.array().items(alertSchema),
+            uri: joi.string().required(),
+          })
+        ),
+        postgres: joi.array().items(
+          joi.alternatives([
+            joi.object({
+              alerts: joi.array().items(alertSchema),
+              uri: joi.string().required(),
+            }),
+            joi.object({
+              alerts: joi.array().items(alertSchema),
+              command: joi.string().allow(''),
+              data: joi
+                .alternatives()
+                .try(joi.string().allow(''), joi.number()),
+              database: joi.string().allow(''),
+              host: joi
+                .alternatives()
+                .try(
+                  joi.string().allow('').hostname(),
+                  joi.string().allow('').ip()
+                )
+                .required(),
+              password: joi.string().allow(''),
+              port: joi.number().default(5432),
+              username: joi.string().allow(''),
+            }),
+          ])
+        ),
+        redis: joi.array().items(
+          joi
+            .object({
+              alerts: joi.array().items(alertSchema),
+              command: joi.string().allow(''),
+              host: joi
+                .alternatives()
+                .try(
+                  joi.string().allow('').hostname(),
+                  joi.string().allow('').ip()
+                ),
+              password: joi.string().allow(''),
+              port: joi.number().default(6379).min(0).max(65_536),
+              uri: joi.string().allow(''),
+              username: joi.string().allow(''),
+            })
+            .xor('host', 'uri')
+            .and('host', 'port')
+        ),
+        requests: joi
+          .array()
+          .min(isSymonModeFrom(getContext().flags) ? 0 : 1)
+          .items(
+            joi.object({
+              alerts: joi.array().items(alertSchema),
+              allowUnauthorized: joi.bool(),
+              body: joi
+                .alternatives()
+                .try(
+                  joi.string().allow('', null),
+                  joi.object(),
+                  joi.array(),
+                  joi.number(),
+                  joi.bool()
+                ),
+              headers: joi.object().allow(null),
+              id: joi.string().allow(''),
+              interval: joi.number().min(1),
+              method: joi
+                .string()
+                .valid(
+                  'GET',
+                  'POST',
+                  'PUT',
+                  'PATCH',
+                  'DELETE',
+                  'HEAD',
+                  'OPTIONS',
+                  'PURGE',
+                  'LINK',
+                  'UNLINK'
+                )
+                .allow('')
+                .default('GET')
+                .insensitive()
+                .label('Probe request method'),
+              ping: joi.bool(),
+              saveBody: joi.bool().default(false),
+              timeout: joi.number().default(10_000).min(1).allow(null),
+              url: joi
+                .string()
+                .custom((url) => {
+                  if (!isValidURL(url)) {
+                    throw new Error(
+                      `Probe request URL (${url}) should start with http:// or https://`
+                    )
+                  }
 
-  for (const probe of probes) {
-    const { name, interval, requests } = probe
+                  return url
+                })
+                .label('Probe request URL')
+                .required(),
+            })
+          )
+          .label('Probe requests'),
+        socket: joi.object({
+          alerts: joi.array().items(alertSchema),
+          data: joi.string().allow('', null),
+          host: joi.string().required(),
+          port: joi.number().required(),
+        }),
+      })
+    )
 
-    if (interval <= 0) {
-      return `The interval in the probe with name "${name}" should be greater than 0.`
-    }
+  try {
+    const validatedProbes = await schema.validateAsync(probes, {
+      stripUnknown: true,
+    })
+
+    for (const probe of validatedProbes) {
+      if (!isProbeRequestExists(probe)) {
+        throw new Error(
+          'Probe requests does not exists or has length lower than 1!'
+        )
+      }
 
-    const totalProbesError = checkTotalProbes(probe)
-    if (totalProbesError) {
-      return totalProbesError
+      validateAlerts(combineAlerts(probe))
     }
 
-    const schemaConfigError = validateSchemaConfig(probe)
-    if (schemaConfigError) {
-      return schemaConfigError
+    return transformDeprecatedAlerts(validatedProbes)
+  } catch (error: any) {
+    throw new Error(`Monika configuration is invalid. Probe: ${error?.message}`)
+  }
+}
+
+function isProbeRequestExists(probe: Probe) {
+  return (
+    probe?.mariadb ||
+    probe?.mongo ||
+    probe?.mysql ||
+    probe?.ping ||
+    probe?.postgres ||
+    probe?.redis ||
+    probe?.requests ||
+    probe?.socket
+  )
+}
+
+function combineAlerts(probe: Probe) {
+  const httpAlerts =
+    probe?.requests?.map(({ alerts }) => alerts).find(Boolean) || []
+  const mariadbAlerts =
+    probe?.mariadb?.map(({ alerts }) => alerts).find(Boolean) || []
+  const mongoAlerts =
+    probe?.mongo?.map(({ alerts }) => alerts).find(Boolean) || []
+  const mysqlAlerts =
+    probe?.mysql?.map(({ alerts }) => alerts).find(Boolean) || []
+  const pingAlerts =
+    probe?.ping?.map(({ alerts }) => alerts).find(Boolean) || []
+  const postgresAlerts =
+    probe?.postgres?.map(({ alerts }) => alerts).find(Boolean) || []
+  const redisAlerts =
+    probe?.redis?.map(({ alerts }) => alerts).find(Boolean) || []
+  const socketAlerts = probe?.socket?.alerts || []
+
+  return [
+    ...(probe?.alerts || []),
+    ...httpAlerts,
+    ...mariadbAlerts,
+    ...mongoAlerts,
+    ...mysqlAlerts,
+    ...pingAlerts,
+    ...postgresAlerts,
+    ...redisAlerts,
+    ...socketAlerts,
+  ]
+}
+
+const ALERT_QUERY = 'status-not-2xx'
+const RESPONSE_TIME_PREFIX = 'response-time-greater-than-'
+
+// parse string like "response-time-greater-than-200-ms" and return the time in ms
+function parseAlertStringTime(str: string): number {
+  // match any string that ends with digits followed by unit 's' or 'ms'
+  const match = str.match(/(\d+)-(m?s)$/)
+
+  if (!match) {
+    throw new Error('Alert string does not contain valid time number')
+  }
+
+  const number = Number(match[1])
+  const unit = match[2]
+
+  if (unit === 's') return number * 1000
+
+  return number
+}
+
+function validateAlerts(alerts: (ProbeAlert | string)[]) {
+  for (const alert of alerts) {
+    try {
+      isValidProbeAlert(alert)
+    } catch {
+      throw new Error(`Probe alert format is invalid! (${alert})`)
     }
+  }
+}
+
+function isValidProbeAlert(alert: ProbeAlert | string) {
+  if (typeof alert !== 'string') {
+    compileExpression(alert.assertion || alert.query || '')
+    return
+  }
+
+  // Legacy alerts
+  if (alert === ALERT_QUERY) {
+    return
+  }
+
+  if (alert.startsWith(RESPONSE_TIME_PREFIX)) {
+    parseAlertStringTime(alert)
+    return
+  }
 
-    const validateRequestsError = validateRequests(requests)
-    if (validateRequestsError) {
-      return validateRequestsError
+  throw new Error('Unknown alert')
+}
+
+function transformDeprecatedAlerts(probes: Probe[]) {
+  return probes.map((probe) => {
+    const newAlerts = []
+
+    if (!probe?.alerts) {
+      return probe
     }
 
-    const validateAlertError = validateAlerts(probe)
-    if (validateAlertError) {
-      return validateAlertError
+    for (const alert of probe.alerts) {
+      if (typeof alert !== 'string') {
+        newAlerts.push(alert)
+        continue
+      }
+
+      if (alert === ALERT_QUERY) {
+        newAlerts.push({
+          id: uuid(),
+          assertion: 'response.status < 200 or response.status > 299',
+          message: 'HTTP status is {{ response.status }}, expecting 2xx',
+        })
+        continue
+      }
+
+      if ((alert as string).startsWith(RESPONSE_TIME_PREFIX)) {
+        const expectedTime = parseAlertStringTime(alert)
+
+        newAlerts.push({
+          id: uuid(),
+          assertion: `response.time > ${expectedTime}`,
+          message: `Response time is {{ response.time }}ms, expecting less than ${expectedTime}ms`,
+        })
+      }
     }
-  }
+
+    return { ...probe, alerts: newAlerts }
+  })
 }
diff --git a/src/components/config/validation/validator/redis-config.ts b/src/components/config/validation/validator/redis-config.ts
deleted file mode 100644
index eb66139a..00000000
--- a/src/components/config/validation/validator/redis-config.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/**********************************************************************************
- * MIT License                                                                    *
- *                                                                                *
- * Copyright (c) 2021 Hyperjump Technology                                        *
- *                                                                                *
- * Permission is hereby granted, free of charge, to any person obtaining a copy   *
- * of this software and associated documentation files (the "Software"), to deal  *
- * in the Software without restriction, including without limitation the rights   *
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell      *
- * copies of the Software, and to permit persons to whom the Software is          *
- * furnished to do so, subject to the following conditions:                       *
- *                                                                                *
- * The above copyright notice and this permission notice shall be included in all *
- * copies or substantial portions of the Software.                                *
- *                                                                                *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR     *
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,       *
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE    *
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER         *
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,  *
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE  *
- * SOFTWARE.                                                                      *
- **********************************************************************************/
-
-import Joi from 'joi'
-import { Redis } from '../../../../interfaces/probe'
-
-export const validateRedisConfig = (
-  redisConfig?: Redis[]
-): string | undefined => {
-  if (!redisConfig) {
-    return ''
-  }
-
-  const schema = Joi.object({
-    host: Joi.alternatives().try(Joi.string().hostname(), Joi.string().ip()),
-    port: Joi.number().min(0).max(65_536),
-    password: Joi.string(),
-    username: Joi.string(),
-    command: Joi.string(),
-    uri: Joi.string(),
-  })
-    .xor('host', 'uri')
-    .and('host', 'port')
-
-  for (const redis of redisConfig) {
-    const validationError = schema.validate(redis)
-    if (validationError?.error?.message) return validationError?.error?.message
-  }
-}
diff --git a/src/components/config/validation/validator/request.ts b/src/components/config/validation/validator/request.ts
deleted file mode 100644
index 4b8ceb01..00000000
--- a/src/components/config/validation/validator/request.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-/**********************************************************************************
- * MIT License                                                                    *
- *                                                                                *
- * Copyright (c) 2021 Hyperjump Technology                                        *
- *                                                                                *
- * Permission is hereby granted, free of charge, to any person obtaining a copy   *
- * of this software and associated documentation files (the "Software"), to deal  *
- * in the Software without restriction, including without limitation the rights   *
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell      *
- * copies of the Software, and to permit persons to whom the Software is          *
- * furnished to do so, subject to the following conditions:                       *
- *                                                                                *
- * The above copyright notice and this permission notice shall be included in all *
- * copies or substantial portions of the Software.                                *
- *                                                                                *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR     *
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,       *
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE    *
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER         *
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,  *
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE  *
- * SOFTWARE.                                                                      *
- **********************************************************************************/
-
-import { RequestConfig } from '../../../../interfaces/request'
-import { HTTPMethods } from '../../../../utils/http'
-import { isValidURL } from '../../../../utils/is-valid-url'
-
-const PROBE_REQUEST_INVALID_METHOD =
-  'Probe request method is invalid! Valid methods are GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, PURGE, LINK, and UNLINK'
-const PROBE_REQUEST_NO_URL = 'Probe request URL does not exists'
-
-const checkProbeRequestProperties = (
-  requests: RequestConfig[]
-): string | undefined => {
-  if (requests?.length > 0) {
-    for (const request of requests) {
-      const { method, ping, url } = request
-
-      if (!url) {
-        return PROBE_REQUEST_NO_URL
-      }
-
-      // if not a ping request and url not valid, return INVLID_URL error
-      if (ping !== true && !isValidURL(url)) {
-        return `Probe request URL (${url}) should start with http:// or https://`
-      }
-
-      if (!HTTPMethods.has(method?.toUpperCase() ?? 'GET'))
-        return PROBE_REQUEST_INVALID_METHOD
-    }
-  }
-}
-
-export const validateRequests = (
-  requests?: RequestConfig[]
-): string | undefined => {
-  if (requests === undefined) {
-    return
-  }
-
-  for (const req of requests) {
-    if (req.timeout <= 0) {
-      return `The timeout in the request with id "${req.id}" should be greater than 0.`
-    }
-  }
-
-  const probeRequestPropertyMessage = checkProbeRequestProperties(requests)
-  if (probeRequestPropertyMessage) {
-    return probeRequestPropertyMessage
-  }
-}
diff --git a/src/components/config/validation/validator/schema-config.ts b/src/components/config/validation/validator/schema-config.ts
deleted file mode 100644
index b8b07ebd..00000000
--- a/src/components/config/validation/validator/schema-config.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/**********************************************************************************
- * MIT License                                                                    *
- *                                                                                *
- * Copyright (c) 2021 Hyperjump Technology                                        *
- *                                                                                *
- * Permission is hereby granted, free of charge, to any person obtaining a copy   *
- * of this software and associated documentation files (the "Software"), to deal  *
- * in the Software without restriction, including without limitation the rights   *
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell      *
- * copies of the Software, and to permit persons to whom the Software is          *
- * furnished to do so, subject to the following conditions:                       *
- *                                                                                *
- * The above copyright notice and this permission notice shall be included in all *
- * copies or substantial portions of the Software.                                *
- *                                                                                *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR     *
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,       *
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE    *
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER         *
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,  *
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE  *
- * SOFTWARE.                                                                      *
- **********************************************************************************/
-
-import { Probe } from '../../../../interfaces/probe'
-import { validateMongoConfig } from './mongo-config'
-import { validateRedisConfig } from './redis-config'
-import { validateTCPConfig } from './tcp-config'
-
-export const validateSchemaConfig = (probe: Probe): string | undefined => {
-  const { socket, redis, mongo } = probe
-  const tcpConfigError = validateTCPConfig(socket)
-  if (tcpConfigError) {
-    return `Monika configuration: probes.socket ${tcpConfigError}`
-  }
-
-  const redisConfigError = validateRedisConfig(redis)
-  if (redisConfigError) {
-    return `Monika configuration: probes.redis ${redisConfigError}`
-  }
-
-  const mongoConfigError = validateMongoConfig(mongo)
-  if (mongoConfigError) {
-    return `Monika configuration: probes.mongo ${mongoConfigError}`
-  }
-}
diff --git a/src/components/config/validation/validator/tcp-config.ts b/src/components/config/validation/validator/tcp-config.ts
deleted file mode 100644
index 82c0eaa4..00000000
--- a/src/components/config/validation/validator/tcp-config.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/**********************************************************************************
- * MIT License                                                                    *
- *                                                                                *
- * Copyright (c) 2021 Hyperjump Technology                                        *
- *                                                                                *
- * Permission is hereby granted, free of charge, to any person obtaining a copy   *
- * of this software and associated documentation files (the "Software"), to deal  *
- * in the Software without restriction, including without limitation the rights   *
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell      *
- * copies of the Software, and to permit persons to whom the Software is          *
- * furnished to do so, subject to the following conditions:                       *
- *                                                                                *
- * The above copyright notice and this permission notice shall be included in all *
- * copies or substantial portions of the Software.                                *
- *                                                                                *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR     *
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,       *
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE    *
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER         *
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,  *
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE  *
- * SOFTWARE.                                                                      *
- **********************************************************************************/
-
-import Joi from 'joi'
-import { Socket } from '../../../../interfaces/probe'
-
-export const validateTCPConfig = (tcpConfig?: Socket): string | undefined => {
-  if (!tcpConfig) {
-    return ''
-  }
-
-  const schema = Joi.object({
-    host: Joi.string().required(),
-    port: Joi.number().required(),
-    data: Joi.string(),
-  })
-  const validationError = schema.validate(tcpConfig)
-
-  return validationError?.error?.message
-}
diff --git a/src/components/probe/index.ts b/src/components/probe/index.ts
index 205c1bbe..06679779 100644
--- a/src/components/probe/index.ts
+++ b/src/components/probe/index.ts
@@ -34,7 +34,6 @@ import {
   setProbeRunning,
 } from '../../utils/probe-state'
 import { createProbers } from './prober/factory'
-// import { stopDowntimeCounter, startDowntimeCounter } from '../downtime-counter'
 
 type doProbeParams = {
   probe: Probe // probe contains all the probes
diff --git a/src/interfaces/probe.ts b/src/interfaces/probe.ts
index 61c5a7b7..4a269dd7 100644
--- a/src/interfaces/probe.ts
+++ b/src/interfaces/probe.ts
@@ -66,6 +66,7 @@ export type Mongo = {
   port?: number
   username?: string
   password?: string
+  alerts?: ProbeAlert[]
 }
 
 export type Postgres = {
@@ -82,6 +83,7 @@ export type Postgres = {
 
 export type Ping = {
   uri: string
+  alerts?: ProbeAlert[]
 }
 
 export interface Probe {
diff --git a/src/plugins/validate-response/checkers/index.ts b/src/plugins/validate-response/checkers/index.ts
index 3eac2fb5..adc78b81 100644
--- a/src/plugins/validate-response/checkers/index.ts
+++ b/src/plugins/validate-response/checkers/index.ts
@@ -22,27 +22,10 @@
  * SOFTWARE.                                                                      *
  **********************************************************************************/
 
-import { ProbeAlert } from '../../../interfaces/probe'
-import { ProbeRequestResponse } from '../../../interfaces/request'
+import type { ProbeAlert } from '../../../interfaces/probe'
+import type { ProbeRequestResponse } from '../../../interfaces/request'
 import queryExpression from './query-expression'
 
-// parse string like "response-time-greater-than-200-ms" and return the time in ms
-export const parseAlertStringTime = (str: string): number => {
-  // match any string that ends with digits followed by unit 's' or 'ms'
-  const match = str.match(/(\d+)-(m?s)$/)
-
-  if (!match) {
-    throw new Error('Alert string does not contain valid time number')
-  }
-
-  const number = Number(match[1])
-  const unit = match[2]
-
-  if (unit === 's') return number * 1000
-
-  return number
-}
-
 // responseChecker checks some result and returns boolean if alert requered
 const responseChecker = (
   alert: ProbeAlert,
diff --git a/src/symon/index.ts b/src/symon/index.ts
index 7a985cd1..fc30a618 100644
--- a/src/symon/index.ts
+++ b/src/symon/index.ts
@@ -34,7 +34,7 @@ import path from 'path'
 import { updateConfig } from '../components/config'
 import { getOSName } from '../components/notification/alert-message'
 import { getContext } from '../context'
-import type { MonikaFlags } from '../flag'
+import { SYMON_API_VERSION, type MonikaFlags } from '../flag'
 import events from '../events'
 import { Config } from '../interfaces/config'
 import { Probe } from '../interfaces/probe'
@@ -49,6 +49,7 @@ import {
   publicIpAddress,
   publicNetworkInfo,
 } from '../utils/public-ip'
+import { validateProbes } from '../components/config/validation'
 
 Bree.extend(require('@breejs/ts-worker'))
 
@@ -183,7 +184,7 @@ class SymonClient {
     symonMonikaId,
     symonReportInterval,
     symonReportLimit,
-    'symon-api-version': apiVersion,
+    'symon-api-version': apiVersion = SYMON_API_VERSION.v1,
   }: Pick<
     MonikaFlags,
     | 'symon-api-version'
@@ -340,16 +341,18 @@ class SymonClient {
           return [200, 304].includes(status)
         },
       })
-      .then((res) => {
-        if (res.data.data) {
-          this.probes = res.data.data
+      .then(async (res) => {
+        if (!res.data.data) {
+          log.info('No config changes from Symon')
 
-          log.debug(`Received ${res.data.data.length} probes`)
-        } else {
-          log.debug(`No new config from Symon`)
+          return { probes: this.probes, hash: res.headers.etag }
         }
 
-        return { probes: res.data.data, hash: res.headers.etag }
+        const validatedProbes = await validateProbes(res.data.data)
+        this.probes = validatedProbes
+        log.info(`Received ${validatedProbes.length} probes`)
+
+        return { probes: validatedProbes, hash: res.headers.etag }
       })
       .catch((error) => {
         if (error.isAxiosError) {
diff --git a/src/utils/http.ts b/src/utils/http.ts
index 829b4adc..24d2ca62 100644
--- a/src/utils/http.ts
+++ b/src/utils/http.ts
@@ -32,19 +32,6 @@ const httpAgent = new http.Agent({ keepAlive: true })
 const httpsAgent = new https.Agent({ keepAlive: true })
 export const DEFAULT_TIMEOUT = 10_000
 
-export const HTTPMethods = new Set([
-  'DELETE',
-  'GET',
-  'HEAD',
-  'OPTIONS',
-  'PATCH',
-  'POST',
-  'PUT',
-  'PURGE',
-  'LINK',
-  'UNLINK',
-])
-
 // Create an instance of axios here so it will be reused instead of creating a new one all the time.
 const axiosInstance = axios.create()
 


Test patch:
diff --git a/src/components/config/__tests__/expected.har.yml b/src/components/config/__tests__/expected.har.yml
index 4cbaef4f..2efb9b15 100644
--- a/src/components/config/__tests__/expected.har.yml
+++ b/src/components/config/__tests__/expected.har.yml
@@ -1,5 +1,6 @@
 probes:
-  - requests:
+  - id: https://namb.ch/api/admin/login
+    requests:
       - method: POST
         url: https://namb.ch/api/admin/login
         headers:
diff --git a/src/components/config/index.test.ts b/src/components/config/index.test.ts
index 7b2b2ead..8819b14c 100644
--- a/src/components/config/index.test.ts
+++ b/src/components/config/index.test.ts
@@ -30,9 +30,10 @@ import events from '../../events'
 import { md5Hash } from '../../utils/hash'
 import { getEventEmitter } from '../../utils/events'
 import type { MonikaFlags } from '../../flag'
+import { validateProbes } from './validation'
 
 describe('getConfig', () => {
-  afterEach(() => {
+  beforeEach(() => {
     resetContext()
   })
 
@@ -42,6 +43,19 @@ describe('getConfig', () => {
     )
   })
 
+  it('should not throw error when config is empty in symon mode', () => {
+    // arrange
+    setContext({
+      flags: {
+        symonKey: 'bDF8j',
+        symonUrl: 'https://example.com',
+      } as MonikaFlags,
+    })
+
+    // act & assert
+    expect(getConfig()).deep.eq({ probes: [] })
+  })
+
   it('should return empty array when config is empty in Symon mode', () => {
     // arrange
     setContext({
@@ -122,7 +136,7 @@ describe('isSymonModeFrom', () => {
 })
 
 describe('updateConfig', () => {
-  afterEach(() => {
+  beforeEach(() => {
     resetContext()
   })
 
@@ -138,7 +152,7 @@ describe('updateConfig', () => {
     }
 
     // assert
-    expect(errorMessage).eq(
+    expect(errorMessage).contain(
       'Probes object does not exists or has length lower than 1!'
     )
   })
@@ -156,6 +170,10 @@ describe('updateConfig', () => {
         },
       ],
     }
+    const validatedProbes = {
+      ...config,
+      probes: await validateProbes(config.probes),
+    }
     let configFromEmitter = ''
     const eventEmitter = getEventEmitter()
     eventEmitter.once(events.config.updated, (data) => {
@@ -167,13 +185,13 @@ describe('updateConfig', () => {
 
     // assert
     expect(getContext().config).to.deep.eq({
-      ...config,
-      version: md5Hash(config),
+      ...validatedProbes,
+      version: md5Hash(validatedProbes),
     })
     expect(configFromEmitter).not.eq('')
   })
 
-  it('should should not update config', async () => {
+  it('should should not update config if there is no changes', async () => {
     // arrange
     const config: Config = {
       probes: [
@@ -186,17 +204,16 @@ describe('updateConfig', () => {
         },
       ],
     }
-    setContext({ config: { ...config, version: md5Hash(config) } })
     let configFromEmitter = ''
     const eventEmitter = getEventEmitter()
-    const eventListener = (data: any) => {
+    const eventListener = (data: string) => {
       configFromEmitter = data
     }
 
-    eventEmitter.once(events.config.updated, eventListener)
-
     // act
     await updateConfig(config)
+    eventEmitter.once(events.config.updated, eventListener)
+    await updateConfig(config)
 
     // assert
     expect(configFromEmitter).eq('')
diff --git a/src/components/probe/index.test.ts b/src/components/probe/index.test.ts
index 9c5e078e..d5e34b02 100644
--- a/src/components/probe/index.test.ts
+++ b/src/components/probe/index.test.ts
@@ -36,7 +36,6 @@ import * as httpRequest from '../../utils/http'
 import { doProbe } from '.'
 import { initializeProbeStates } from '../../utils/probe-state'
 import type { Probe } from '../../interfaces/probe'
-import { afterEach, beforeEach } from 'mocha'
 import { getContext, resetContext, setContext } from '../../context'
 import type { MonikaFlags } from '../../flag'
 import { FAILED_REQUEST_ASSERTION } from '../../looper'
@@ -138,8 +137,6 @@ describe('Probe processing', () => {
       // wait for random timeout
       await sleep(3 * seconds)
 
-      resetContext()
-
       // assert
       expect(urlRequestTotal).eq(1)
     })
@@ -374,7 +371,7 @@ describe('Probe processing', () => {
         mariadb: [
           {
             host: 'localhost',
-            port: 3306,
+            port: 3307,
             username: 'mariadb_user',
             password: 'mariadb_password',
             database: '',
@@ -431,7 +428,7 @@ describe('Probe processing', () => {
         mariadb: [
           {
             host: 'localhost',
-            port: 3306,
+            port: 3308,
             username: 'mariadb_user',
             password: 'mariadb_password',
             database: '',
diff --git a/src/symon/index.test.ts b/src/symon/index.test.ts
index 99c3c87e..c936866b 100644
--- a/src/symon/index.test.ts
+++ b/src/symon/index.test.ts
@@ -23,72 +23,94 @@
  **********************************************************************************/
 
 import { expect } from '@oclif/test'
-import { RequestInterceptor } from 'node-request-interceptor'
-import withDefaultInterceptors from 'node-request-interceptor/lib/presets/default'
-import SymonClient from '.'
+import { rest } from 'msw'
+import { setupServer } from 'msw/node'
 import sinon from 'sinon'
-import Stun from 'stun'
-import { Config } from '../interfaces/config'
+
+import type { Config } from '../interfaces/config'
+import type { MonikaFlags } from '../flag'
+
 import * as loggerHistory from '../components/logger/history'
 import { setContext } from '../context'
-import { type MonikaFlags, SYMON_API_VERSION } from '../flag'
+import SymonClient from '.'
+import { validateProbes } from '../components/config/validation'
 
-let interceptor: RequestInterceptor
-let testStunStub: sinon.SinonStub
 let getUnreportedLogsStub: sinon.SinonStub
 
-beforeEach(() => {
-  interceptor = new RequestInterceptor(withDefaultInterceptors)
+const server = setupServer(
+  rest.get('http://ip-api.com/json/192.168.1.1', (_, res, ctx) =>
+    res(
+      ctx.json({
+        city: 'jakarta',
+        isp: 'hyperjump',
+        country: 'Indonesia',
+      })
+    )
+  ),
+  rest.post(
+    'http://localhost:4000/api/v1/monika/client-handshake',
+    (_, res, ctx) =>
+      res(
+        ctx.json({
+          statusCode: 'ok',
+          message: 'Successfully handshaked with Symon',
+          data: {
+            monikaId: '1234',
+          },
+        })
+      )
+  ),
+  rest.post('http://localhost:4000/api/v1/monika/status', (_, res, ctx) =>
+    res(ctx.status(200))
+  ),
+  rest.get('http://localhost:4000/api/v1/monika/report', (_, res, ctx) =>
+    res(
+      ctx.json({
+        statusCode: 'ok',
+        message: 'Successfully report to Symon',
+      })
+    )
+  )
+)
 
-  interceptor.use((req) => {
-    // mock the call to get isp and city
-    if (req.url.origin === 'http://ip-api.com') {
-      return {
-        status: 200,
-        body: JSON.stringify({
-          city: 'jakarta',
-          isp: 'hyperjump',
-          country: 'Indonesia',
-        }),
-      }
-    }
+describe('Symon initiate', () => {
+  before(() => {
+    server.listen()
   })
+  beforeEach(() => {
+    setContext({
+      flags: {
+        symonUrl: 'https://example.com',
+        symonKey: 'random-key',
+      } as MonikaFlags,
+    })
 
-  // mock the stun request
-  testStunStub = sinon.stub(Stun, 'request').resolves({
-    getXorAddress: () => {
-      return {
-        address: '192.168.1.1',
-      }
-    },
+    getUnreportedLogsStub = sinon
+      .stub(loggerHistory, 'getUnreportedLogs')
+      .resolves({ requests: [], notifications: [] })
+  })
+  afterEach(() => {
+    server.resetHandlers()
+    getUnreportedLogsStub.restore()
+  })
+  after(() => {
+    server.close()
   })
 
-  getUnreportedLogsStub = sinon
-    .stub(loggerHistory, 'getUnreportedLogs')
-    .resolves({ requests: [], notifications: [] })
-})
-
-afterEach(() => {
-  interceptor.restore()
-  testStunStub.restore()
-  getUnreportedLogsStub.restore()
-})
+  const config: Config = {
+    version: 'asdfg123',
+    probes: [
+      {
+        id: '1',
+        name: 'test',
+        interval: 10,
+        requests: [],
+        alerts: [],
+      },
+    ],
+  }
 
-describe('Symon initiate', () => {
   it('should send handshake data on initiate', async () => {
-    const config: Config = {
-      version: 'asdfg123',
-      probes: [
-        {
-          id: '1',
-          name: 'test',
-          interval: 10,
-          requests: [],
-          alerts: [],
-        },
-      ],
-    }
-
     setContext({
       userAgent: 'v1.5.0',
       flags: {
@@ -96,40 +118,38 @@ describe('Symon initiate', () => {
         symonKey: 'random-key',
       } as MonikaFlags,
     })
-    let sentBody = ''
+    let body: any
     // mock the outgoing requests
-    interceptor.use((req) => {
-      // mock the handshake to symon
-      if (req.url.origin === 'http://localhost:4000') {
-        if (req.url.pathname.endsWith('client-handshake')) {
-          sentBody = req.body!
-          return {
-            status: 200,
-            body: JSON.stringify({
+    server.use(
+      rest.post(
+        'http://localhost:4000/api/v1/monika/client-handshake',
+        async (req, res, ctx) => {
+          body = await req.json()
+
+          return res(
+            ctx.json({
               statusCode: 'ok',
               message: 'Successfully handshaked with Symon',
               data: {
                 monikaId: '1234',
               },
-            }),
-          }
+            })
+          )
         }
-
-        if (req.url.pathname.endsWith('probes')) {
-          return {
-            status: 200,
-            headers: {
-              etag: config.version as string,
-            },
-            body: JSON.stringify({
+      ),
+      rest.get(
+        'http://localhost:4000/api/v1/monika/1234/probes',
+        (_, res, ctx) =>
+          res(
+            ctx.set('etag', config.version || ''),
+            ctx.json({
               statusCode: 'ok',
               message: 'Successfully get probes configuration',
               data: config.probes,
-            }),
-          }
-        }
-      }
-    })
+            })
+          )
+      )
+    )
 
     const symon = new SymonClient({
       symonUrl: 'http://localhost:4000',
@@ -141,7 +161,6 @@ describe('Symon initiate', () => {
     await symon.stopReport()
     expect(symon.monikaId).equals('1234')
 
-    const body = JSON.parse(sentBody)
     expect(body.publicIp).equals('192.168.1.1')
     expect(body.pid).greaterThan(0)
     expect(body.macAddress).length.greaterThan(0)
@@ -155,59 +174,20 @@ describe('Symon initiate', () => {
   })
 
   it('should fetch probes config on initiate', async () => {
-    const config: Config = {
-      version: 'asdfg123',
-      probes: [
-        {
-          id: '1',
-          name: 'test',
-          interval: 10,
-          requests: [],
-          alerts: [],
-        },
-      ],
-    }
-
-    interceptor.use((req) => {
-      if (req.url.origin === 'http://localhost:4000') {
-        if (req.url.pathname.endsWith('client-handshake')) {
-          return {
-            status: 200,
-            body: JSON.stringify({
-              statusCode: 'ok',
-              message: 'Successfully handshaked with Symon',
-              data: {
-                monikaId: '1234',
-              },
-            }),
-          }
-        }
-
-        if (req.url.pathname.endsWith('probes')) {
-          return {
-            status: 200,
-            headers: {
-              etag: config.version as string,
-            },
-            body: JSON.stringify({
+    server.use(
+      rest.get(
+        'http://localhost:4000/api/v1/monika/1234/probes',
+        (_, res, ctx) =>
+          res(
+            ctx.set('etag', config.version || ''),
+            ctx.json({
               statusCode: 'ok',
               message: 'Successfully get probes configuration',
               data: config.probes,
-            }),
-          }
-        }
-
-        if (req.url.pathname.endsWith('report')) {
-          return {
-            status: 200,
-            body: JSON.stringify({
-              statusCode: 'ok',
-              message: 'Successfully report to Symon',
-            }),
-          }
-        }
-      }
-    })
+            })
+          )
+      )
+    )
 
     const symon = new SymonClient({
       symonUrl: 'http://localhost:4000',
@@ -220,33 +200,30 @@ describe('Symon initiate', () => {
     await symon.initiate()
     await symon.stopReport()
 
-    expect(symon.config).deep.equals(config)
+    expect(symon.config).deep.equals({
+      ...config,
+      probes: await validateProbes(config.probes),
+    })
   })
 
   it('should throw an error if the request to get probes is failed', async () => {
     // arrange
-    interceptor.use((req) => {
-      if (req.url.origin === 'http://localhost:4000') {
-        if (req.url.pathname.endsWith('client-handshake')) {
-          return {
-            status: 200,
-            body: JSON.stringify({
+    server.resetHandlers()
+    server.use(
+      rest.post(
+        'http://localhost:4000/api/v1/monika/client-handshake',
+        (_, res, ctx) =>
+          res(
+            ctx.json({
               statusCode: 'ok',
               message: 'Successfully handshaked with Symon',
               data: {
                 monikaId: '1234',
               },
-            }),
-          }
-        }
-
-        if (req.url.pathname.endsWith('status')) {
-          return {
-            status: 200,
-          }
-        }
-      }
-    })
+            })
+          )
+      )
+    )
 
     const symon = new SymonClient({
       symonUrl: 'http://localhost:4000',
@@ -261,95 +238,74 @@ describe('Symon initiate', () => {
       errorMessage = error?.message
     }
 
-    // arrange
+    // assert
     expect(errorMessage).eq('Failed to get probes from Symon')
   })
 
   it('should report on initiate', async () => {
-    interceptor.use((req) => {
-      if (req.url.origin === 'http://localhost:4000') {
-        if (req.url.pathname.endsWith('client-handshake')) {
-          return {
-            status: 200,
-            body: JSON.stringify({
-              statusCode: 'ok',
-              message: 'Successfully handshaked with Symon',
-              data: {
-                monikaId: '1234',
-              },
-            }),
-          }
-        }
-
-        if (req.url.pathname.endsWith('probes')) {
-          return {
-            status: 200,
-            body: JSON.stringify({
+    // arrange
+    server.use(
+      rest.get(
+        'http://localhost:4000/api/v1/monika/1234/probes',
+        (_, res, ctx) =>
+          res(
+            ctx.set('etag', config.version || ''),
+            ctx.json({
               statusCode: 'ok',
               message: 'Successfully get probes configuration',
-            }),
-          }
-        }
-
-        if (req.url.pathname.endsWith('report')) {
-          return {
-            status: 200,
-            body: JSON.stringify({
-              statusCode: 'ok',
-              message: 'Successfully report to Symon',
-            }),
-          }
-        }
-      }
-    })
-
+              data: config.probes,
+            })
+          )
+      )
+    )
     const symon = new SymonClient({
       symonUrl: 'http://localhost:4000',
       symonKey: 'abcd',
     })
     const reportSpy = sinon.spy(symon, 'report')
 
+    // act
     await symon.initiate()
     await symon.stopReport()
 
+    // assert
     expect(reportSpy.called).equals(true)
   })
-})
 
-describe('Send incident or recovery event', () => {
   it('should send event to Symon when incident or recovery happens', async () => {
-    let sentBody = ''
-
-    // mock the outgoing requests
-    interceptor.use((req) => {
-      if (req.url.href === 'http://localhost:4000/api/v1/monika/events') {
-        sentBody = req.body as string
-
-        return {
-          status: 200,
-          body: JSON.stringify({
-            message: 'Successfully send incident event to Symon',
-            data: null,
-          }),
+    // arrange
+    let body: any
+    server.use(
+      rest.post(
+        'http://localhost:4000/api/v1/monika/events',
+        async (req, res, ctx) => {
+          body = await req.json()
+
+          return res(
+            ctx.json({
+              message: 'Successfully send incident event to Symon',
+              data: null,
+            })
+          )
         }
-      }
-    })
+      )
+    )
 
     const symon = new SymonClient({
       symonUrl: 'http://localhost:4000',
       symonKey: 'abcd',
-      'symon-api-version': SYMON_API_VERSION.v1,
     })
     sinon.spy(symon, 'report')
     symon.monikaId = '1234'
 
+    // act
     await symon.notifyEvent({
       event: 'incident',
       alertId: 'alert86',
       response: { status: 400, time: 1000 },
     })
 
-    const body = JSON.parse(sentBody)
+    // assert
     expect(body.monikaId).equals('1234')
     expect(body.event).equals('incident')
     expect(body.alertId).equals('alert86')
diff --git a/test/index.test.ts b/test/index.test.ts
index d4b4ae3e..37ca8522 100644
--- a/test/index.test.ts
+++ b/test/index.test.ts
@@ -164,7 +164,7 @@ describe('monika', () => {
     )
     .catch((error) => {
       expect(error.message).to.contain(
-        'Probe request method is invalid! Valid methods are GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, PURGE, LINK, and UNLINK'
+        '"Probe request method" must be one of [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, PURGE, LINK, UNLINK, ]'
       )
     })
     .it('runs with config with invalid probe request method')
diff --git a/test/testConfigs/teams/teamsconfigNoURL.yml b/test/testConfigs/teams/teamsconfigNoURL.yml
index b45d3926..e0f3ce41 100644
--- a/test/testConfigs/teams/teamsconfigNoURL.yml
+++ b/test/testConfigs/teams/teamsconfigNoURL.yml
@@ -3,5 +3,6 @@ notifications:
     type: teams
     data: {}
 probes:
-  - requests:
+  - id: teams-probe-01
+    requests:
       - url: https://github.com