Repository: hyperjumptech/monika
Pull request #1035 (2023-04-19 07:01:26)

Refactoring: Extract the Get Config Function
# Monika Pull Request (PR)  

## What feature/issue does this PR add  
As a part of refactoring, I extracted the get config function to make it reusable in other part of the code

## How did you implement / how did you fix it  
1. Split the sanitize flag, get config, merge config, and update config phase
2. Extract the get config function

## How to test  
1. Run the test
2. Run Monika as usual


codecov[bot]: ## [Codecov](https://codecov.io/gh/hyperjumptech/monika/pull/1035?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) Report
> Merging [#1035](https://codecov.io/gh/hyperjumptech/monika/pull/1035?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) (2cb6a53) into [main](https://codecov.io/gh/hyperjumptech/monika/commit/02591df2f7ed0e6dc96df2edd97db1b402d511ec?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) (02591df) will **increase** coverage by `0.20%`.
> The diff coverage is `67.08%`.

> :exclamation: Current head 2cb6a53 differs from pull request most recent head 0502707. Consider uploading reports for the commit 0502707 to get more accurate results

```diff
@@            Coverage Diff             @@
##             main    #1035      +/-   ##
==========================================
+ Coverage   49.69%   49.90%   +0.20%     
==========================================
  Files         109      110       +1     
  Lines        2990     3006      +16     
  Branches      578      579       +1     
==========================================
+ Hits         1486     1500      +14     
  Misses       1321     1321              
- Partials      183      185       +2     
```


| [Impacted Files](https://codecov.io/gh/hyperjumptech/monika/pull/1035?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech) | Coverage Δ | |
|---|---|---|
| [src/components/config/index.ts](https://codecov.io/gh/hyperjumptech/monika/pull/1035?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2NvbXBvbmVudHMvY29uZmlnL2luZGV4LnRz) | `61.66% <42.42%> (-4.34%)` | :arrow_down: |
| [src/components/config/get.ts](https://codecov.io/gh/hyperjumptech/monika/pull/1035?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperjumptech#diff-c3JjL2NvbXBvbmVudHMvY29uZmlnL2dldC50cw==) | `84.78% <84.78%> (ø)` | |

: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)


Patch:
diff --git a/src/components/config/get.ts b/src/components/config/get.ts
new file mode 100644
index 00000000..e3fb6105
--- /dev/null
+++ b/src/components/config/get.ts
@@ -0,0 +1,123 @@
+import type { MonikaFlags } from '../../context/monika-flags'
+import type { Config } from '../../interfaces/config'
+import { log } from '../../utils/pino'
+import { parseConfig } from './parse'
+
+export type ConfigType =
+  | 'monika'
+  | 'har'
+  | 'insomnia'
+  | 'postman'
+  | 'sitemap'
+  | 'text'
+
+export async function getConfigFrom(flags: MonikaFlags): Promise<Config> {
+  const defaultConfigs = await parseDefaultConfig(flags)
+  const nonDefaultConfig = setDefaultNotifications(
+    defaultConfigs,
+    await getNonDefaultFlags(flags)
+  )
+
+  return mergeConfigs(defaultConfigs, nonDefaultConfig)
+}
+
+// mergeConfigs merges configs by overwriting each other
+// with initial value taken from nonDefaultConfig
+export function mergeConfigs(
+  defaultConfigs: Partial<Config>[],
+  nonDefaultConfig: Partial<Config>
+): Config {
+  if (defaultConfigs.length === 0 && nonDefaultConfig !== undefined) {
+    return nonDefaultConfig as Config
+  }
+
+  // eslint-disable-next-line unicorn/no-array-reduce
+  const mergedConfig = defaultConfigs.reduce((prev, current) => {
+    return {
+      ...prev,
+      ...current,
+      notifications: current.notifications || prev.notifications,
+      probes: current.probes || prev.probes,
+    }
+  }, nonDefaultConfig || {})
+
+  return mergedConfig as Config
+}
+
+export function addDefaultNotifications(
+  config: Partial<Config>
+): Partial<Config> {
+  log.info('Notifications not found, using desktop as default...')
+  return {
+    ...config,
+    notifications: [{ id: 'default', type: 'desktop', data: undefined }],
+  }
+}
+
+async function parseDefaultConfig(
+  flags: MonikaFlags
+): Promise<Partial<Config>[]> {
+  return Promise.all(
+    flags.config.map((source) => parseConfigType(source, 'monika', flags))
+  )
+}
+
+async function parseConfigType(
+  source: string,
+  configType: ConfigType,
+  flags: MonikaFlags
+): Promise<Partial<Config>> {
+  const parsed = await parseConfig(source, configType, flags)
+
+  return {
+    ...parsed,
+    probes: parsed.probes?.map((probe) => {
+      const requests =
+        probe?.requests?.map((request) => ({
+          ...request,
+          timeout: request.timeout ?? 10_000,
+        })) ?? []
+
+      const interval = () => {
+        if (typeof probe?.interval === 'number') return probe.interval
+        return requests.length * 10 === 0 ? 10 : requests.length * 10
+      }
+
+      return { ...probe, interval: interval(), requests }
+    }),
+  }
+}
+
+async function getNonDefaultFlags(
+  flags: MonikaFlags
+): Promise<Partial<Config>> {
+  let result = {}
+
+  if (flags.har) {
+    result = await parseConfigType(flags.har, 'har', flags)
+  } else if (flags.postman) {
+    result = await parseConfigType(flags.postman, 'postman', flags)
+  } else if (flags.insomnia) {
+    result = await parseConfigType(flags.insomnia, 'insomnia', flags)
+  } else if (flags.sitemap) {
+    result = await parseConfigType(flags.sitemap, 'sitemap', flags)
+  } else if (flags.text) {
+    result = await parseConfigType(flags.text, 'text', flags)
+  }
+
+  return result
+}
+
+function setDefaultNotifications(
+  defaultConfigs: Partial<Config>[],
+  nonDefaultConfig: Partial<Config>
+): Partial<Config> {
+  const hasDefaultConfig = defaultConfigs.length > 0
+  const hasNonDefaultConfig = Object.keys(nonDefaultConfig).length > 0
+
+  if (!hasDefaultConfig && hasNonDefaultConfig) {
+    return addDefaultNotifications(nonDefaultConfig)
+  }
+
+  return nonDefaultConfig
+}
diff --git a/src/components/config/index.ts b/src/components/config/index.ts
index 941e91ac..f409d469 100644
--- a/src/components/config/index.ts
+++ b/src/components/config/index.ts
@@ -41,9 +41,15 @@ import { validateConfig } from './validate'
 import { createConfigFile } from './create-config'
 import yml from 'js-yaml'
 import { exit } from 'process'
+import {
+  type ConfigType,
+  addDefaultNotifications,
+  getConfigFrom,
+  mergeConfigs,
+} from './get'
 
 type ScheduleRemoteConfigFetcherParams = {
-  configType: 'monika' | 'har' | 'insomnia' | 'postman' | 'sitemap' | 'text'
+  configType: ConfigType
   interval: number
   url: string
   index?: number
@@ -56,6 +62,7 @@ type WatchConfigFileParams = {
   repeat?: number
 }
 
+const isTestEnvironment = process.env.CI || process.env.NODE_ENV === 'test'
 const emitter = getEventEmitter()
 
 let cfg: Config
@@ -76,7 +83,7 @@ export async function* getConfigIterator(
 
   yield cfg
 
-  if (!(process.env.CI || process.env.NODE_ENV === 'test')) {
+  if (!isTestEnvironment) {
     yield* pEvent.iterator<string, Config>(emitter, events.config.updated)
   }
 }
@@ -90,7 +97,7 @@ export const updateConfig = async (
     try {
       await validateConfig(config)
     } catch (error: any) {
-      if (process.env.NODE_ENV === 'test') {
+      if (isTestEnvironment) {
         // return error during tests
         throw new Error(error.message)
       }
@@ -109,45 +116,81 @@ export const updateConfig = async (
   }
 }
 
-// mergeConfigs merges global configs var by overwriting each other
-// with initial value taken from nonDefaultConfig
-const mergeConfigs = (): Config => {
-  if (defaultConfigs.length === 0 && nonDefaultConfig !== undefined) {
-    return nonDefaultConfig as Config
-  }
+export const setupConfig = async (flags: MonikaFlags): Promise<void> => {
+  const sanitizedFlags = await sanitizeFlags(flags)
+  const config = await getConfigFrom(sanitizedFlags)
 
-  // eslint-disable-next-line unicorn/no-array-reduce
-  const mergedConfig = defaultConfigs.reduce((prev, current) => {
-    return {
-      ...prev,
-      ...current,
-      notifications: current.notifications || prev.notifications,
-      probes: current.probes || prev.probes,
-    }
-  }, nonDefaultConfig || {})
+  watchConfigsChange(sanitizedFlags)
 
-  return mergedConfig as Config
+  await updateConfig(config)
 }
 
-function watchConfigFile({ path, type, index, repeat }: WatchConfigFileParams) {
-  const isWatchConfigFile = !(
-    process.env.CI ||
-    process.env.NODE_ENV === 'test' ||
-    repeat !== 0
+async function sanitizeFlags(flags: MonikaFlags): Promise<MonikaFlags> {
+  // check for default config path when -c/--config not provided
+  const hasConfig =
+    flags.config.length > 0 ||
+    flags.har ||
+    flags.postman ||
+    flags.insomnia ||
+    flags.sitemap ||
+    flags.text
+
+  if (!hasConfig) {
+    log.info('No Monika configuration available, initializing...')
+    const configFilename = await createConfigFile(flags)
+    return { ...flags, config: [configFilename] }
+  }
+
+  return flags
+}
+
+async function watchConfigsChange(flags: MonikaFlags) {
+  await Promise.all(
+    flags.config.map((source, index) =>
+      watchConfigChange({
+        interval:
+          flags['config-interval'] ||
+          monikaFlagsDefaultValue['config-interval'],
+        repeat: flags.repeat,
+        source,
+        type: 'monika',
+        index,
+      })
+    )
   )
-  if (isWatchConfigFile) {
-    const watcher = chokidar.watch(path)
-    watcher.on('change', async () => {
-      const newConfig = await parseConfig(path, type)
-      if (index === undefined) {
-        nonDefaultConfig = newConfig
-      } else {
-        defaultConfigs[index] = newConfig
-      }
+}
 
-      await updateConfig(mergeConfigs())
+type WatchConfigChangeParams = {
+  interval: number
+  repeat: number
+  source: string
+  type: ConfigType
+  index?: number
+}
+
+function watchConfigChange({
+  interval,
+  repeat,
+  source,
+  type,
+  index,
+}: WatchConfigChangeParams) {
+  if (isUrl(source)) {
+    scheduleRemoteConfigFetcher({
+      configType: type,
+      interval,
+      url: source,
+      index,
     })
+    return
   }
+
+  watchConfigFile({
+    path: source,
+    type,
+    index,
+    repeat,
+  })
 }
 
 function scheduleRemoteConfigFetcher({
@@ -165,105 +208,28 @@ function scheduleRemoteConfigFetcher({
         defaultConfigs[index] = newConfig
       }
 
-      await updateConfig(mergeConfigs())
+      await updateConfig(mergeConfigs(defaultConfigs, nonDefaultConfig))
     } catch (error: any) {
       log.error(error?.message)
     }
   }, interval * 1000)
 }
 
-const parseConfigType = async (
-  source: string,
-  configType: 'monika' | 'har' | 'insomnia' | 'postman' | 'sitemap' | 'text',
-  flags: MonikaFlags,
-  index?: number
-): Promise<Partial<Config>> => {
-  if (isUrl(source)) {
-    const interval: number =
-      flags['config-interval'] || monikaFlagsDefaultValue['config-interval']
-    scheduleRemoteConfigFetcher({ configType, interval, url: source, index })
-  } else {
-    watchConfigFile({
-      path: source,
-      type: configType,
-      index,
-      repeat: flags.repeat,
-    })
-  }
-
-  const parsed = await parseConfig(source, configType, flags)
-
-  return {
-    ...parsed,
-    probes: parsed.probes?.map((probe) => {
-      const requests =
-        probe?.requests?.map((request) => ({
-          ...request,
-          timeout: request.timeout ?? 10_000,
-        })) ?? []
-
-      const interval = () => {
-        if (typeof probe?.interval === 'number') return probe.interval
-        return requests.length * 10 === 0 ? 10 : requests.length * 10
+function watchConfigFile({ path, type, index, repeat }: WatchConfigFileParams) {
+  const isWatchConfigFile = !(isTestEnvironment || repeat !== 0)
+  if (isWatchConfigFile) {
+    const watcher = chokidar.watch(path)
+    watcher.on('change', async () => {
+      const newConfig = await parseConfig(path, type)
+      if (index === undefined) {
+        nonDefaultConfig = newConfig
+      } else {
+        defaultConfigs[index] = newConfig
       }
 
-      return { ...probe, interval: interval(), requests }
-    }),
-  }
-}
-
-const parseDefaultConfig = async (
-  flags: MonikaFlags
-): Promise<Partial<Config>[]> => {
-  return Promise.all(
-    (flags.config as Array<string>).map((source, index) =>
-      parseConfigType(source, 'monika', flags, index)
-    )
-  )
-}
-
-const addDefaultNotifications = (config: Partial<Config>): Partial<Config> => {
-  log.info('Notifications not found, using desktop as default...')
-  return {
-    ...config,
-    notifications: [{ id: 'default', type: 'desktop', data: undefined }],
-  }
-}
-
-export const setupConfig = async (flags: MonikaFlags): Promise<void> => {
-  // check for default config path when -c/--config not provided
-  if (
-    flags.config.length === 0 &&
-    flags.har === undefined &&
-    flags.postman === undefined &&
-    flags.insomnia === undefined &&
-    flags.sitemap === undefined &&
-    flags.text === undefined
-  ) {
-    log.info(`No Monika configuration available, initializing...`)
-    const configFilename = await createConfigFile(flags)
-    flags.config = [configFilename]
-  }
-
-  defaultConfigs = await parseDefaultConfig(flags)
-
-  if (flags.har) {
-    nonDefaultConfig = await parseConfigType(flags.har, 'har', flags)
-  } else if (flags.postman) {
-    nonDefaultConfig = await parseConfigType(flags.postman, 'postman', flags)
-  } else if (flags.insomnia) {
-    nonDefaultConfig = await parseConfigType(flags.insomnia, 'insomnia', flags)
-  } else if (flags.sitemap) {
-    nonDefaultConfig = await parseConfigType(flags.sitemap, 'sitemap', flags)
-  } else if (flags.text) {
-    nonDefaultConfig = await parseConfigType(flags.text, 'text', flags)
-  }
-
-  if (defaultConfigs.length === 0 && nonDefaultConfig !== undefined) {
-    nonDefaultConfig = addDefaultNotifications(nonDefaultConfig)
+      await updateConfig(mergeConfigs(defaultConfigs, nonDefaultConfig))
+    })
   }
-
-  await updateConfig(mergeConfigs())
 }
 
 const getPathAndTypeFromFlag = (flags: MonikaFlags) => {
@@ -323,7 +289,7 @@ export const createConfig = async (flags: MonikaFlags): Promise<void> => {
     }
 
     const parse = await parseConfig(path, type, flags)
-    const result = await addDefaultNotifications(parse)
+    const result = addDefaultNotifications(parse)
     const file = flags.output || 'monika.yml'
 
     if (existsSync(file) && !flags.force) {


Test patch:
diff --git a/src/components/config/get.test.ts b/src/components/config/get.test.ts
new file mode 100644
index 00000000..d9942ecb
--- /dev/null
+++ b/src/components/config/get.test.ts
@@ -0,0 +1,75 @@
+/**********************************************************************************
+ * 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 { expect } from '@oclif/test'
+import type { Config } from '../../interfaces/config'
+import { addDefaultNotifications } from './get'
+
+describe('Add default notification', () => {
+  it('should add default notification', () => {
+    // arrange
+    const config: Partial<Config> = {
+      probes: [],
+    }
+
+    // act
+    const newConfig = addDefaultNotifications(config)
+
+    // assert
+    expect(newConfig).deep.eq({
+      probes: [],
+      notifications: [{ id: 'default', type: 'desktop', data: undefined }],
+    })
+  })
+
+  it('should add default notification (empty config)', () => {
+    // arrange
+    const config: Partial<Config> = {}
+
+    // act
+    const newConfig = addDefaultNotifications(config)
+
+    // assert
+    expect(newConfig).deep.eq({
+      notifications: [{ id: 'default', type: 'desktop', data: undefined }],
+    })
+  })
+
+  it('should override existing notification', () => {
+    // arrange
+    const config: Partial<Config> = {
+      notifications: [
+        { id: '1', type: 'webhook', data: 'https://example.com' },
+      ],
+    }
+
+    // act
+    const newConfig = addDefaultNotifications(config)
+
+    // assert
+    expect(newConfig).deep.eq({
+      notifications: [{ id: 'default', type: 'desktop', data: undefined }],
+    })
+  })
+})