feat: inspections (#3213)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
@@ -192,6 +192,7 @@
|
|||||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||||
"shortcodes": "Shortcodes are empty",
|
"shortcodes": "Shortcodes are empty",
|
||||||
"subscription": "Subscriptions are empty",
|
"subscription": "Subscriptions are empty",
|
||||||
|
"suggestions": "No matching suggestions found",
|
||||||
"team_name": "Team name empty",
|
"team_name": "Team name empty",
|
||||||
"teams": "You don't belong to any teams",
|
"teams": "You don't belong to any teams",
|
||||||
"tests": "There are no tests for this request"
|
"tests": "There are no tests for this request"
|
||||||
@@ -304,6 +305,30 @@
|
|||||||
"preview": "Hide Preview",
|
"preview": "Hide Preview",
|
||||||
"sidebar": "Collapse sidebar"
|
"sidebar": "Collapse sidebar"
|
||||||
},
|
},
|
||||||
|
"inspections": {
|
||||||
|
"title": "Inspector",
|
||||||
|
"description": "Inspect possible errors",
|
||||||
|
"environment": {
|
||||||
|
"add_environment": "Add to Environment",
|
||||||
|
"not_found": "Environment variable “{environment}” not found."
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"401_error": "Please check your authentication credentials.",
|
||||||
|
"404_error": "Please check your request URL and method type.",
|
||||||
|
"network_error": "Please check your network connection.",
|
||||||
|
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
|
||||||
|
"default_error": "Please check your request."
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"extension_not_installed": "Extension not installed.",
|
||||||
|
"extention_not_enabled": "Extension not enabled.",
|
||||||
|
"extention_enable_action": "Enable Browser Extension",
|
||||||
|
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list."
|
||||||
|
}
|
||||||
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"collections": "Import collections",
|
"collections": "Import collections",
|
||||||
"curl": "Import cURL",
|
"curl": "Import cURL",
|
||||||
|
|||||||
35
packages/hoppscotch-common/src/components.d.ts
vendored
35
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -14,6 +14,7 @@ declare module '@vue/runtime-core' {
|
|||||||
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
||||||
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
|
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
|
||||||
AppHeader: typeof import('./components/app/Header.vue')['default']
|
AppHeader: typeof import('./components/app/Header.vue')['default']
|
||||||
|
AppInspection: typeof import('./components/app/Inspection.vue')['default']
|
||||||
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
|
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
|
||||||
AppLogo: typeof import('./components/app/Logo.vue')['default']
|
AppLogo: typeof import('./components/app/Logo.vue')['default']
|
||||||
AppOptions: typeof import('./components/app/Options.vue')['default']
|
AppOptions: typeof import('./components/app/Options.vue')['default']
|
||||||
@@ -77,8 +78,27 @@ declare module '@vue/runtime-core' {
|
|||||||
History: typeof import('./components/history/index.vue')['default']
|
History: typeof import('./components/history/index.vue')['default']
|
||||||
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
||||||
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
||||||
|
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||||
|
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||||
|
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
|
||||||
|
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||||
|
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
||||||
|
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
||||||
|
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
||||||
|
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||||
|
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||||
|
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||||
|
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
||||||
|
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
|
||||||
|
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
||||||
|
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
||||||
|
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
||||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||||
|
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||||
|
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
||||||
|
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
||||||
|
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
||||||
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
||||||
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
|
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
|
||||||
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
|
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
|
||||||
@@ -105,6 +125,21 @@ declare module '@vue/runtime-core' {
|
|||||||
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
||||||
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
||||||
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
||||||
|
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
|
||||||
|
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||||
|
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||||
|
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
||||||
|
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||||
|
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||||
|
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||||
|
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
|
||||||
|
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||||
|
IconLucideInfo: typeof import('~icons/lucide/info')['default']
|
||||||
|
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||||
|
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||||
|
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||||
|
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||||
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||||
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
||||||
|
|||||||
112
packages/hoppscotch-common/src/components/app/Inspection.vue
Normal file
112
packages/hoppscotch-common/src/components/app/Inspection.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="inspectionResults && inspectionResults.length > 0">
|
||||||
|
<tippy interactive trigger="click" theme="popover">
|
||||||
|
<div class="flex justify-center items-center flex-1 flex-col">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconAlertTriangle"
|
||||||
|
:class="severityColor(getHighestSeverity.severity)"
|
||||||
|
:title="t('inspections.description')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div class="flex flex-col space-y-2 items-start flex-1">
|
||||||
|
<div
|
||||||
|
class="flex justify-between border rounded pl-2 border-divider bg-popover sticky top-0 self-stretch"
|
||||||
|
>
|
||||||
|
<span class="flex items-center flex-1">
|
||||||
|
<icon-lucide-activity class="mr-2 svg-icons text-accent" />
|
||||||
|
<span class="font-bold">
|
||||||
|
{{ t("inspections.title") }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
to="https://docs.hoppscotch.io/documentation/features/inspections"
|
||||||
|
blank
|
||||||
|
:title="t('app.wiki')"
|
||||||
|
:icon="IconHelpCircle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(inspector, index) in inspectionResults"
|
||||||
|
:key="index"
|
||||||
|
class="flex self-stretch"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-col flex-1 rounded border border-dashed border-dividerDark divide-y divide-dashed divide-dividerDark"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="inspector.text.type === 'text'"
|
||||||
|
class="flex-1 px-3 py-2"
|
||||||
|
>
|
||||||
|
{{ inspector.text.text }}
|
||||||
|
<HoppSmartLink
|
||||||
|
blank
|
||||||
|
:to="inspector.doc.link"
|
||||||
|
class="text-accent hover:text-accentDark transition"
|
||||||
|
>
|
||||||
|
{{ inspector.doc.text }}
|
||||||
|
<icon-lucide-arrow-up-right class="svg-icons" />
|
||||||
|
</HoppSmartLink>
|
||||||
|
</span>
|
||||||
|
<span v-if="inspector.action" class="flex p-2 space-x-2">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="inspector.action.text"
|
||||||
|
outline
|
||||||
|
filled
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
inspector.action?.apply()
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { InspectorResult } from "~/services/inspection"
|
||||||
|
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||||
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
inspectionResults: InspectorResult[] | undefined
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const getHighestSeverity = computed(() => {
|
||||||
|
if (props.inspectionResults) {
|
||||||
|
return props.inspectionResults.reduce(
|
||||||
|
(prev, curr) => {
|
||||||
|
return prev.severity > curr.severity ? prev : curr
|
||||||
|
},
|
||||||
|
{ severity: 0 }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return { severity: 0 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const severityColor = (severity: number) => {
|
||||||
|
switch (severity) {
|
||||||
|
case 1:
|
||||||
|
return "!text-green-500 hover:!text-green-600"
|
||||||
|
case 2:
|
||||||
|
return "!text-yellow-500 hover:!text-yellow-600"
|
||||||
|
case 3:
|
||||||
|
return "!text-red-500 hover:!text-red-600"
|
||||||
|
default:
|
||||||
|
return "!text-gray-500 hover:!text-gray-600"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -79,16 +79,13 @@
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<HoppSmartAutoComplete
|
<SmartEnvInput
|
||||||
|
v-model="header.key"
|
||||||
:placeholder="`${t('count.header', { count: index + 1 })}`"
|
:placeholder="`${t('count.header', { count: index + 1 })}`"
|
||||||
:source="commonHeaders"
|
:auto-complete-source="commonHeaders"
|
||||||
:spellcheck="false"
|
:env-index="index"
|
||||||
:value="header.key"
|
:inspection-results="getInspectorResult(headerKeyResults, index)"
|
||||||
autofocus
|
@change="
|
||||||
styles=" bg-transparent flex flex-1
|
|
||||||
py-1 px-4 truncate "
|
|
||||||
class="flex-1 !flex"
|
|
||||||
@input="
|
|
||||||
updateHeader(index, {
|
updateHeader(index, {
|
||||||
id: header.id,
|
id: header.id,
|
||||||
key: $event,
|
key: $event,
|
||||||
@@ -100,6 +97,10 @@
|
|||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="header.value"
|
v-model="header.value"
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
:inspection-results="
|
||||||
|
getInspectorResult(headerValueResults, index)
|
||||||
|
"
|
||||||
|
:env-index="index"
|
||||||
@change="
|
@change="
|
||||||
updateHeader(index, {
|
updateHeader(index, {
|
||||||
id: header.id,
|
id: header.id,
|
||||||
@@ -265,6 +266,9 @@ import {
|
|||||||
} from "~/helpers/utils/EffectiveURL"
|
} from "~/helpers/utils/EffectiveURL"
|
||||||
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
|
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { InspectionService, InspectorResult } from "~/services/inspection"
|
||||||
|
import { currentTabID } from "~/helpers/rest/tab"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -502,4 +506,39 @@ const changeTab = (tab: ComputedHeader["source"]) => {
|
|||||||
if (tab === "auth") emit("change-tab", "authorization")
|
if (tab === "auth") emit("change-tab", "authorization")
|
||||||
else emit("change-tab", "bodyParams")
|
else emit("change-tab", "bodyParams")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inspectionService = useService(InspectionService)
|
||||||
|
|
||||||
|
const allTabResults = inspectionService.tabs
|
||||||
|
|
||||||
|
const headerKeyResults = computed(() => {
|
||||||
|
return (
|
||||||
|
allTabResults.value
|
||||||
|
.get(currentTabID.value)
|
||||||
|
.filter(
|
||||||
|
(result) =>
|
||||||
|
result.locations.type === "header" &&
|
||||||
|
result.locations.position === "key"
|
||||||
|
) ?? []
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const headerValueResults = computed(() => {
|
||||||
|
return (
|
||||||
|
allTabResults.value
|
||||||
|
.get(currentTabID.value)
|
||||||
|
.filter(
|
||||||
|
(result) =>
|
||||||
|
result.locations.type === "header" &&
|
||||||
|
result.locations.position === "value"
|
||||||
|
) ?? []
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getInspectorResult = (results: InspectorResult[], index: number) => {
|
||||||
|
return results.filter((result) => {
|
||||||
|
if (result.locations.type === "url" || result.locations.type === "response")
|
||||||
|
return
|
||||||
|
return result.locations.index === index
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -82,6 +82,9 @@
|
|||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="param.key"
|
v-model="param.key"
|
||||||
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
|
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
|
||||||
|
:inspection-results="
|
||||||
|
getInspectorResult(parameterKeyResults, index)
|
||||||
|
"
|
||||||
@change="
|
@change="
|
||||||
updateParam(index, {
|
updateParam(index, {
|
||||||
id: param.id,
|
id: param.id,
|
||||||
@@ -94,6 +97,9 @@
|
|||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="param.value"
|
v-model="param.value"
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
:inspection-results="
|
||||||
|
getInspectorResult(parameterValueResults, index)
|
||||||
|
"
|
||||||
@change="
|
@change="
|
||||||
updateParam(index, {
|
updateParam(index, {
|
||||||
id: param.id,
|
id: param.id,
|
||||||
@@ -173,7 +179,7 @@ import IconCheckCircle from "~icons/lucide/check-circle"
|
|||||||
import IconCircle from "~icons/lucide/circle"
|
import IconCircle from "~icons/lucide/circle"
|
||||||
import IconTrash from "~icons/lucide/trash"
|
import IconTrash from "~icons/lucide/trash"
|
||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
import IconWrapText from "~icons/lucide/wrap-text"
|
||||||
import { reactive, ref, watch } from "vue"
|
import { computed, reactive, ref, watch } from "vue"
|
||||||
import { flow, pipe } from "fp-ts/function"
|
import { flow, pipe } from "fp-ts/function"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
@@ -195,6 +201,9 @@ import { useToast } from "@composables/toast"
|
|||||||
import { throwError } from "@functional/error"
|
import { throwError } from "@functional/error"
|
||||||
import { objRemoveKey } from "@functional/object"
|
import { objRemoveKey } from "@functional/object"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { InspectionService, InspectorResult } from "~/services/inspection"
|
||||||
|
import { currentTabID } from "~/helpers/rest/tab"
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
@@ -398,4 +407,39 @@ const clearContent = () => {
|
|||||||
|
|
||||||
bulkParams.value = ""
|
bulkParams.value = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inspectionService = useService(InspectionService)
|
||||||
|
|
||||||
|
const allTabResults = inspectionService.tabs
|
||||||
|
|
||||||
|
const parameterKeyResults = computed(() => {
|
||||||
|
return (
|
||||||
|
allTabResults.value
|
||||||
|
.get(currentTabID.value)
|
||||||
|
.filter(
|
||||||
|
(result) =>
|
||||||
|
result.locations.type === "parameter" &&
|
||||||
|
result.locations.position === "key"
|
||||||
|
) ?? []
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const parameterValueResults = computed(() => {
|
||||||
|
return (
|
||||||
|
allTabResults.value
|
||||||
|
.get(currentTabID.value)
|
||||||
|
.filter(
|
||||||
|
(result) =>
|
||||||
|
result.locations.type === "parameter" &&
|
||||||
|
result.locations.position === "value"
|
||||||
|
) ?? []
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getInspectorResult = (results: InspectorResult[], index: number) => {
|
||||||
|
return results.filter((result) => {
|
||||||
|
if (result.locations.type === "url" || result.locations.type === "response")
|
||||||
|
return
|
||||||
|
return result.locations.index === index
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -53,9 +53,16 @@
|
|||||||
v-model="tab.document.request.endpoint"
|
v-model="tab.document.request.endpoint"
|
||||||
:placeholder="`${t('request.url')}`"
|
:placeholder="`${t('request.url')}`"
|
||||||
:auto-complete-source="userHistories"
|
:auto-complete-source="userHistories"
|
||||||
|
:inspection-results="tabResults"
|
||||||
@paste="onPasteUrl($event)"
|
@paste="onPasteUrl($event)"
|
||||||
@enter="newSendRequest"
|
@enter="newSendRequest"
|
||||||
/>
|
>
|
||||||
|
<template #empty>
|
||||||
|
<span>
|
||||||
|
{{ t("empty.history_suggestions") }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</SmartEnvInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mt-2 sm:mt-0">
|
<div class="flex mt-2 sm:mt-0">
|
||||||
@@ -259,12 +266,14 @@ import IconLink2 from "~icons/lucide/link-2"
|
|||||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||||
import IconSave from "~icons/lucide/save"
|
import IconSave from "~icons/lucide/save"
|
||||||
import IconShare2 from "~icons/lucide/share-2"
|
import IconShare2 from "~icons/lucide/share-2"
|
||||||
import { HoppRESTTab } from "~/helpers/rest/tab"
|
import { HoppRESTTab, currentTabID } from "~/helpers/rest/tab"
|
||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
|
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { getCurrentStrategyID } from "~/helpers/network"
|
import { getCurrentStrategyID } from "~/helpers/network"
|
||||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { InspectionService } from "~/services/inspection"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -628,4 +637,12 @@ const isCustomMethod = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
||||||
|
|
||||||
|
const inspectionService = useService(InspectionService)
|
||||||
|
|
||||||
|
const allTabResults = inspectionService.tabs
|
||||||
|
|
||||||
|
const tabResults = computed(() => {
|
||||||
|
return allTabResults.value.get(currentTabID.value) ?? []
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1 relative">
|
||||||
<HttpResponseMeta :response="tab.response" />
|
<HttpResponseMeta :response="tab.response" />
|
||||||
<LensesResponseBodyRenderer
|
<LensesResponseBodyRenderer
|
||||||
v-if="!loading && hasResponse"
|
v-if="!loading && hasResponse"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex items-start justify-center flex-shrink-0 p-4 overflow-auto overflow-x-auto bg-primary whitespace-nowrap"
|
class="sticky top-0 z-10 flex items-center justify-center flex-shrink-0 p-4 overflow-auto overflow-x-auto bg-primary whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<AppShortcutsPrompt v-if="response == null" class="flex-1" />
|
<AppShortcutsPrompt v-if="response == null" class="flex-1" />
|
||||||
<div v-else class="flex flex-col flex-1">
|
<div v-else class="flex flex-col flex-1">
|
||||||
@@ -70,6 +70,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<AppInspection
|
||||||
|
v-if="response?.type !== 'loading'"
|
||||||
|
:inspection-results="tabResults"
|
||||||
|
:class="[
|
||||||
|
response === null || response?.type === 'network_fail'
|
||||||
|
? 'absolute right-2 top-2'
|
||||||
|
: 'ml-2 -m-2',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -80,6 +89,9 @@ import type { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
|||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
|
import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { InspectionService } from "~/services/inspection"
|
||||||
|
import { currentTabID } from "~/helpers/rest/tab"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
@@ -128,4 +140,16 @@ const statusCategory = computed(() => {
|
|||||||
}
|
}
|
||||||
return findStatusGroup(props.response.statusCode)
|
return findStatusGroup(props.response.statusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const inspectionService = useService(InspectionService)
|
||||||
|
|
||||||
|
const allTabResults = inspectionService.tabs
|
||||||
|
|
||||||
|
const tabResults = computed(() => {
|
||||||
|
return (
|
||||||
|
allTabResults.value
|
||||||
|
.get(currentTabID.value)
|
||||||
|
?.filter((result) => result.locations.type === "response") ?? []
|
||||||
|
)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
@keydown="handleKeystroke"
|
@keydown="handleKeystroke"
|
||||||
@focusin="showSuggestionPopover = true"
|
@focusin="showSuggestionPopover = true"
|
||||||
></div>
|
></div>
|
||||||
|
<AppInspection :inspection-results="inspectionResults" />
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
v-if="showSuggestionPopover && autoCompleteSource"
|
v-if="showSuggestionPopover && autoCompleteSource"
|
||||||
@@ -34,8 +35,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="suggestions.length === 0" class="pointer-events-none">
|
<li v-if="suggestions.length === 0" class="pointer-events-none">
|
||||||
<span class="truncate py-0.5">
|
<div v-if="slots.empty" class="truncate py-0.5">
|
||||||
{{ t("empty.history_suggestions") }}
|
<slot name="empty"></slot>
|
||||||
|
</div>
|
||||||
|
<span v-else class="truncate py-0.5">
|
||||||
|
{{ t("empty.suggestions") }}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -43,7 +47,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch, nextTick, computed, Ref } from "vue"
|
import { ref, onMounted, watch, nextTick, computed, Ref, useSlots } from "vue"
|
||||||
import {
|
import {
|
||||||
EditorView,
|
EditorView,
|
||||||
placeholder as placeholderExt,
|
placeholder as placeholderExt,
|
||||||
@@ -62,6 +66,7 @@ import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
|||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { useI18n } from "~/composables/i18n"
|
import { useI18n } from "~/composables/i18n"
|
||||||
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
||||||
|
import { InspectorResult } from "~/services/inspection"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -75,6 +80,7 @@ const props = withDefaults(
|
|||||||
environmentHighlights?: boolean
|
environmentHighlights?: boolean
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
autoCompleteSource?: string[]
|
autoCompleteSource?: string[]
|
||||||
|
inspectionResults?: InspectorResult[] | undefined
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
modelValue: "",
|
modelValue: "",
|
||||||
@@ -85,6 +91,8 @@ const props = withDefaults(
|
|||||||
readonly: false,
|
readonly: false,
|
||||||
environmentHighlights: true,
|
environmentHighlights: true,
|
||||||
autoCompleteSource: undefined,
|
autoCompleteSource: undefined,
|
||||||
|
inspectionResult: undefined,
|
||||||
|
inspectionResults: undefined,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,6 +106,8 @@ const emit = defineEmits<{
|
|||||||
(e: "click", ev: any): void
|
(e: "click", ev: any): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const slots = useSlots()
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const cachedValue = ref(props.modelValue)
|
const cachedValue = ref(props.modelValue)
|
||||||
@@ -142,7 +152,9 @@ const suggestions = computed(() => {
|
|||||||
const updateModelValue = (value: string) => {
|
const updateModelValue = (value: string) => {
|
||||||
emit("update:modelValue", value)
|
emit("update:modelValue", value)
|
||||||
emit("change", value)
|
emit("change", value)
|
||||||
showSuggestionPopover.value = false
|
nextTick(() => {
|
||||||
|
showSuggestionPopover.value = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeystroke = (ev: KeyboardEvent) => {
|
const handleKeystroke = (ev: KeyboardEvent) => {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted, onBeforeUnmount, onBeforeMount } from "vue"
|
import { ref, onMounted, onBeforeUnmount, onBeforeMount, watch } from "vue"
|
||||||
import { safelyExtractRESTRequest } from "@hoppscotch/data"
|
import { safelyExtractRESTRequest } from "@hoppscotch/data"
|
||||||
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
||||||
import { useRoute } from "vue-router"
|
import { useRoute } from "vue-router"
|
||||||
@@ -136,6 +136,12 @@ import {
|
|||||||
changeCurrentSyncStatus,
|
changeCurrentSyncStatus,
|
||||||
currentSyncingStatus$,
|
currentSyncingStatus$,
|
||||||
} from "~/newstore/syncing"
|
} from "~/newstore/syncing"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { InspectionService } from "~/services/inspection"
|
||||||
|
import { HeaderInspectorService } from "~/services/inspection/inspectors/header.inspector"
|
||||||
|
import { EnvironmentInspectorService } from "~/services/inspection/inspectors/environment.inspector"
|
||||||
|
import { URLInspectorService } from "~/services/inspection/inspectors/url.inspector"
|
||||||
|
import { ResponseInspectorService } from "~/services/inspection/inspectors/response.inspector"
|
||||||
|
|
||||||
const savingRequest = ref(false)
|
const savingRequest = ref(false)
|
||||||
const confirmingCloseForTabID = ref<string | null>(null)
|
const confirmingCloseForTabID = ref<string | null>(null)
|
||||||
@@ -215,6 +221,7 @@ const removeTab = (tabID: string) => {
|
|||||||
confirmingCloseForTabID.value = tabID
|
confirmingCloseForTabID.value = tabID
|
||||||
} else {
|
} else {
|
||||||
closeTab(tab.value.id)
|
closeTab(tab.value.id)
|
||||||
|
inspectionService.deleteTabInspectorResult(tab.value.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +278,7 @@ const renameReqName = () => {
|
|||||||
const onCloseConfirmSaveTab = () => {
|
const onCloseConfirmSaveTab = () => {
|
||||||
if (!savingRequest.value && confirmingCloseForTabID.value) {
|
if (!savingRequest.value && confirmingCloseForTabID.value) {
|
||||||
closeTab(confirmingCloseForTabID.value)
|
closeTab(confirmingCloseForTabID.value)
|
||||||
|
inspectionService.deleteTabInspectorResult(confirmingCloseForTabID.value)
|
||||||
confirmingCloseForTabID.value = null
|
confirmingCloseForTabID.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,4 +457,18 @@ oAuthURL()
|
|||||||
defineActionHandler("rest.request.open", ({ doc }) => {
|
defineActionHandler("rest.request.open", ({ doc }) => {
|
||||||
createNewTab(doc)
|
createNewTab(doc)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const inspectionService = useService(InspectionService)
|
||||||
|
useService(HeaderInspectorService)
|
||||||
|
useService(EnvironmentInspectorService)
|
||||||
|
useService(URLInspectorService)
|
||||||
|
useService(ResponseInspectorService)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => currentTabID.value,
|
||||||
|
() => {
|
||||||
|
inspectionService.initializeTabInspectors()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { describe, it, expect } from "vitest"
|
||||||
|
import { Inspector, InspectionService, InspectorResult } from "../"
|
||||||
|
import { TestContainer } from "dioc/testing"
|
||||||
|
|
||||||
|
const inspectorResultMock: InspectorResult[] = [
|
||||||
|
{
|
||||||
|
id: "result1",
|
||||||
|
text: { type: "text", text: "Sample Text" },
|
||||||
|
icon: {},
|
||||||
|
isApplicable: true,
|
||||||
|
severity: 2,
|
||||||
|
locations: { type: "url" },
|
||||||
|
doc: { text: "Sample Doc", link: "https://example.com" },
|
||||||
|
action: {
|
||||||
|
text: "Sample Action",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
apply: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const testInspector: Inspector = {
|
||||||
|
inspectorID: "inspector1",
|
||||||
|
getInspectorFor: () => inspectorResultMock,
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("InspectionService", () => {
|
||||||
|
describe("registerInspector", () => {
|
||||||
|
it("should register an inspector", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const service = container.bind(InspectionService)
|
||||||
|
|
||||||
|
service.registerInspector(testInspector)
|
||||||
|
|
||||||
|
expect(service.inspectors.has(testInspector.inspectorID)).toEqual(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("deleteTabInspectorResult", () => {
|
||||||
|
it("should delete a tab's inspector results", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const service = container.bind(InspectionService)
|
||||||
|
|
||||||
|
const tabID = "testTab"
|
||||||
|
service.tabs.value.set(tabID, inspectorResultMock)
|
||||||
|
|
||||||
|
expect(service.tabs.value.has(tabID)).toEqual(true)
|
||||||
|
|
||||||
|
service.deleteTabInspectorResult(tabID)
|
||||||
|
|
||||||
|
expect(service.tabs.value.has(tabID)).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
135
packages/hoppscotch-common/src/services/inspection/index.ts
Normal file
135
packages/hoppscotch-common/src/services/inspection/index.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { Service } from "dioc"
|
||||||
|
import { Component, Ref, ref, watch } from "vue"
|
||||||
|
import { currentActiveTab, currentTabID } from "~/helpers/rest/tab"
|
||||||
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines how to render the text in an Inspector Result
|
||||||
|
*/
|
||||||
|
export type InspectorTextType<T extends object | Component = never> =
|
||||||
|
| {
|
||||||
|
type: "text"
|
||||||
|
text: string[] | string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "custom"
|
||||||
|
component: T
|
||||||
|
componentProps: T extends Component<infer Props> ? Props : never
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InspectorLocation =
|
||||||
|
| {
|
||||||
|
type: "url"
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "header"
|
||||||
|
position: "key" | "value"
|
||||||
|
key?: string
|
||||||
|
index?: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "parameter"
|
||||||
|
position: "key" | "value"
|
||||||
|
key?: string
|
||||||
|
index?: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "body"
|
||||||
|
key: string
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "response"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines info about an inspector result so the UI can render it
|
||||||
|
*/
|
||||||
|
export interface InspectorResult {
|
||||||
|
id: string
|
||||||
|
text: InspectorTextType<any>
|
||||||
|
icon: object | Component
|
||||||
|
severity: number
|
||||||
|
isApplicable: boolean
|
||||||
|
action?: {
|
||||||
|
text: string
|
||||||
|
apply: () => void
|
||||||
|
}
|
||||||
|
doc: {
|
||||||
|
text: string
|
||||||
|
link: string
|
||||||
|
}
|
||||||
|
locations: InspectorLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the state of the inspector service
|
||||||
|
*/
|
||||||
|
export type InspectorState = {
|
||||||
|
results: InspectorResult[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines an inspector that can be registered with the inspector service
|
||||||
|
* Inspectors are used to perform checks on a request and return the results
|
||||||
|
*/
|
||||||
|
export interface Inspector {
|
||||||
|
/**
|
||||||
|
* The unique ID of the inspector
|
||||||
|
*/
|
||||||
|
inspectorID: string
|
||||||
|
/**
|
||||||
|
* Returns the inspector results for the request
|
||||||
|
* @param req The request to inspect
|
||||||
|
* @param res The response to inspect
|
||||||
|
* @returns The inspector results
|
||||||
|
*/
|
||||||
|
getInspectorFor: (
|
||||||
|
req: HoppRESTRequest,
|
||||||
|
res?: HoppRESTResponse
|
||||||
|
) => InspectorResult[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the inspection service
|
||||||
|
* The service watches the current active tab and returns the inspector results for the request and response
|
||||||
|
*/
|
||||||
|
export class InspectionService extends Service {
|
||||||
|
public static readonly ID = "INSPECTION_SERVICE"
|
||||||
|
|
||||||
|
private inspectors: Map<string, Inspector> = new Map()
|
||||||
|
|
||||||
|
public tabs: Ref<Map<string, InspectorResult[]>> = ref(new Map())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a inspector with the inspection service
|
||||||
|
* @param inspector The inspector instance to register
|
||||||
|
*/
|
||||||
|
public registerInspector(inspector: Inspector) {
|
||||||
|
this.inspectors.set(inspector.inspectorID, inspector)
|
||||||
|
}
|
||||||
|
|
||||||
|
public initializeTabInspectors() {
|
||||||
|
watch(
|
||||||
|
currentActiveTab.value,
|
||||||
|
(tab) => {
|
||||||
|
if (!tab) return
|
||||||
|
const req = currentActiveTab.value.document.request
|
||||||
|
const res = currentActiveTab.value.response
|
||||||
|
const inspectors = Array.from(this.inspectors.values()).map((x) =>
|
||||||
|
x.getInspectorFor(req, res)
|
||||||
|
)
|
||||||
|
this.tabs.value.set(
|
||||||
|
currentTabID.value,
|
||||||
|
inspectors.flatMap((x) => x)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public deleteTabInspectorResult(tabID: string) {
|
||||||
|
this.tabs.value.delete(tabID)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { TestContainer } from "dioc/testing"
|
||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { EnvironmentInspectorService } from "../environment.inspector"
|
||||||
|
import { InspectionService } from "../../index"
|
||||||
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
|
|
||||||
|
vi.mock("~/modules/i18n", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
getI18n: () => (x: string) => x,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("~/newstore/environments", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
getAggregateEnvs: () => [{ key: "EXISTING_ENV_VAR", value: "test_value" }],
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("EnvironmentInspectorService", () => {
|
||||||
|
it("registers with the inspection service upon initialization", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const registerInspectorFn = vi.fn()
|
||||||
|
|
||||||
|
container.bindMock(InspectionService, {
|
||||||
|
registerInspector: registerInspectorFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
|
expect(registerInspectorFn).toHaveBeenCalledOnce()
|
||||||
|
expect(registerInspectorFn).toHaveBeenCalledWith(envInspector)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getInspectorFor", () => {
|
||||||
|
it("should return an inspector result when the URL contains undefined environment variables", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "<<UNDEFINED_ENV_VAR>>",
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = envInspector.getInspectorFor(req)
|
||||||
|
|
||||||
|
expect(result).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "environment",
|
||||||
|
isApplicable: true,
|
||||||
|
text: {
|
||||||
|
type: "text",
|
||||||
|
text: "inspections.environment.not_found",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not return an inspector result when the URL contains defined environment variables", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "<<EXISTING_ENV_VAR>>",
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = envInspector.getInspectorFor(req)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return an inspector result when the headers contain undefined environment variables", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
headers: [
|
||||||
|
{ key: "<<UNDEFINED_ENV_VAR>>", value: "some-value", active: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = envInspector.getInspectorFor(req)
|
||||||
|
|
||||||
|
expect(result).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "environment",
|
||||||
|
isApplicable: true,
|
||||||
|
text: {
|
||||||
|
type: "text",
|
||||||
|
text: "inspections.environment.not_found",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not return an inspector result when the headers contain defined environment variables", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
headers: [
|
||||||
|
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = envInspector.getInspectorFor(req)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return an inspector result when the params contain undefined environment variables", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
params: [
|
||||||
|
{ key: "<<UNDEFINED_ENV_VAR>>", value: "some-value", active: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = envInspector.getInspectorFor(req)
|
||||||
|
|
||||||
|
expect(result).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "environment",
|
||||||
|
isApplicable: true,
|
||||||
|
text: {
|
||||||
|
type: "text",
|
||||||
|
text: "inspections.environment.not_found",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not return an inspector result when the params contain defined environment variables", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
headers: [],
|
||||||
|
params: [
|
||||||
|
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = envInspector.getInspectorFor(req)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { TestContainer } from "dioc/testing"
|
||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { HeaderInspectorService } from "../header.inspector"
|
||||||
|
import { InspectionService } from "../../index"
|
||||||
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
|
|
||||||
|
vi.mock("~/modules/i18n", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
getI18n: () => (x: string) => x,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("HeaderInspectorService", () => {
|
||||||
|
it("registers with the inspection service upon initialization", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const registerInspectorFn = vi.fn()
|
||||||
|
|
||||||
|
container.bindMock(InspectionService, {
|
||||||
|
registerInspector: registerInspectorFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerInspector = container.bind(HeaderInspectorService)
|
||||||
|
|
||||||
|
expect(registerInspectorFn).toHaveBeenCalledOnce()
|
||||||
|
expect(registerInspectorFn).toHaveBeenCalledWith(headerInspector)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getInspectorFor", () => {
|
||||||
|
it("should return an inspector result when headers contain cookies", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const headerInspector = container.bind(HeaderInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
headers: [{ key: "Cookie", value: "some-cookie", active: true }],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = headerInspector.getInspectorFor(req)
|
||||||
|
|
||||||
|
expect(result).toContainEqual(
|
||||||
|
expect.objectContaining({ id: "header", isApplicable: true })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return an empty array when headers do not contain cookies", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const headerInspector = container.bind(HeaderInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
headers: [{ key: "Authorization", value: "Bearer abcd", active: true }],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = headerInspector.getInspectorFor(req)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { TestContainer } from "dioc/testing"
|
||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { ResponseInspectorService } from "../response.inspector"
|
||||||
|
import { InspectionService } from "../../index"
|
||||||
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
|
|
||||||
|
vi.mock("~/modules/i18n", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
getI18n: () => (x: string) => x,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("ResponseInspectorService", () => {
|
||||||
|
it("registers with the inspection service upon initialization", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const registerInspectorFn = vi.fn()
|
||||||
|
|
||||||
|
container.bindMock(InspectionService, {
|
||||||
|
registerInspector: registerInspectorFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const responseInspector = container.bind(ResponseInspectorService)
|
||||||
|
|
||||||
|
expect(registerInspectorFn).toHaveBeenCalledOnce()
|
||||||
|
expect(registerInspectorFn).toHaveBeenCalledWith(responseInspector)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getInspectorFor", () => {
|
||||||
|
it("should return an empty array when response is undefined", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const responseInspector = container.bind(ResponseInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = responseInspector.getInspectorFor(req, undefined)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return an inspector result when response type is not success or status code is not 200", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const responseInspector = container.bind(ResponseInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
}
|
||||||
|
const res = { type: "network_fail", statusCode: 400 }
|
||||||
|
|
||||||
|
const result = responseInspector.getInspectorFor(req, res)
|
||||||
|
|
||||||
|
expect(result).toContainEqual(
|
||||||
|
expect.objectContaining({ id: "url", isApplicable: true })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle network_fail responses", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const responseInspector = container.bind(ResponseInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
}
|
||||||
|
const res = { type: "network_fail", statusCode: 500 }
|
||||||
|
|
||||||
|
const result = responseInspector.getInspectorFor(req, res)
|
||||||
|
|
||||||
|
expect(result).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
text: { type: "text", text: "inspections.response.network_error" },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle fail responses", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const responseInspector = container.bind(ResponseInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
}
|
||||||
|
const res = { type: "fail", statusCode: 500 }
|
||||||
|
|
||||||
|
const result = responseInspector.getInspectorFor(req, res)
|
||||||
|
|
||||||
|
expect(result).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
text: { type: "text", text: "inspections.response.default_error" },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle 404 responses", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const responseInspector = container.bind(ResponseInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
}
|
||||||
|
const res = { type: "success", statusCode: 404 }
|
||||||
|
|
||||||
|
const result = responseInspector.getInspectorFor(req, res)
|
||||||
|
|
||||||
|
expect(result).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
text: { type: "text", text: "inspections.response.404_error" },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle 401 responses", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const responseInspector = container.bind(ResponseInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
}
|
||||||
|
const res = { type: "success", statusCode: 401 }
|
||||||
|
|
||||||
|
const result = responseInspector.getInspectorFor(req, res)
|
||||||
|
|
||||||
|
expect(result).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
text: { type: "text", text: "inspections.response.401_error" },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle successful responses", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const responseInspector = container.bind(ResponseInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
}
|
||||||
|
const res = { type: "success", statusCode: 200 }
|
||||||
|
|
||||||
|
const result = responseInspector.getInspectorFor(req, res)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { TestContainer } from "dioc/testing"
|
||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { URLInspectorService } from "../url.inspector"
|
||||||
|
import { InspectionService } from "../../index"
|
||||||
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
|
|
||||||
|
vi.mock("~/modules/i18n", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
getI18n: () => (x: string) => x,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("URLInspectorService", () => {
|
||||||
|
it("registers with the inspection service upon initialization", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const registerInspectorFn = vi.fn()
|
||||||
|
|
||||||
|
container.bindMock(InspectionService, {
|
||||||
|
registerInspector: registerInspectorFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const urlInspector = container.bind(URLInspectorService)
|
||||||
|
|
||||||
|
expect(registerInspectorFn).toHaveBeenCalledOnce()
|
||||||
|
expect(registerInspectorFn).toHaveBeenCalledWith(urlInspector)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getInspectorFor", () => {
|
||||||
|
it("should return an inspector result when localhost is in URL and extension is not available", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const urlInspector = container.bind(URLInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://localhost:8000/api/data",
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = urlInspector.getInspectorFor(req)
|
||||||
|
|
||||||
|
expect(result).toContainEqual(
|
||||||
|
expect.objectContaining({ id: "url", isApplicable: true })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not return an inspector result when localhost is not in URL", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
const urlInspector = container.bind(URLInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://example.com/api/data",
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = urlInspector.getInspectorFor(req)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add the correct text to the results when extension is not installed", () => {
|
||||||
|
vi.mock("~/newstore/HoppExtension", async () => {
|
||||||
|
const { BehaviorSubject }: any = await vi.importActual("rxjs")
|
||||||
|
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
extensionStatus$: new BehaviorSubject("waiting"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const container = new TestContainer()
|
||||||
|
const urlInspector = container.bind(URLInspectorService)
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
...getDefaultRESTRequest(),
|
||||||
|
endpoint: "http://localhost:8000/api/data",
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = urlInspector.getInspectorFor(req)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1)
|
||||||
|
expect(result[0]).toMatchObject({
|
||||||
|
text: { type: "text", text: "inspections.url.extension_not_installed" },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { getI18n } from "~/modules/i18n"
|
||||||
|
import {
|
||||||
|
InspectionService,
|
||||||
|
Inspector,
|
||||||
|
InspectorLocation,
|
||||||
|
InspectorResult,
|
||||||
|
} from ".."
|
||||||
|
import { Service } from "dioc"
|
||||||
|
import { Ref, markRaw, ref } from "vue"
|
||||||
|
import IconPlusCircle from "~icons/lucide/plus-circle"
|
||||||
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { getAggregateEnvs } from "~/newstore/environments"
|
||||||
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
|
||||||
|
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
|
||||||
|
|
||||||
|
const isENVInString = (str: string) => {
|
||||||
|
return HOPP_ENVIRONMENT_REGEX.test(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This inspector is responsible for inspecting the environment variables of a input.
|
||||||
|
* It checks if the environment variables are defined in the environment.
|
||||||
|
* It also provides an action to add the environment variable.
|
||||||
|
*
|
||||||
|
* NOTE: Initializing this service registers it as a inspector with the Inspection Service.
|
||||||
|
*/
|
||||||
|
export class EnvironmentInspectorService extends Service implements Inspector {
|
||||||
|
public static readonly ID = "ENVIRONMENT_INSPECTOR_SERVICE"
|
||||||
|
|
||||||
|
private t = getI18n()
|
||||||
|
|
||||||
|
public readonly inspectorID = "environment"
|
||||||
|
|
||||||
|
private readonly inspection = this.bind(InspectionService)
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.inspection.registerInspector(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the environment variables in the target array
|
||||||
|
* @param target The target array to validate
|
||||||
|
* @param results The results array to push the results to
|
||||||
|
* @param locations The location where results are to be displayed
|
||||||
|
* @returns The results array
|
||||||
|
*/
|
||||||
|
private validateEnvironmentVariables = (
|
||||||
|
target: any[],
|
||||||
|
results: Ref<InspectorResult[]>,
|
||||||
|
locations: InspectorLocation
|
||||||
|
) => {
|
||||||
|
const env = getAggregateEnvs()
|
||||||
|
const envKeys = env.map((e) => e.key)
|
||||||
|
|
||||||
|
target.forEach((element, index) => {
|
||||||
|
if (isENVInString(element)) {
|
||||||
|
const extractedEnv = element.match(HOPP_ENVIRONMENT_REGEX)
|
||||||
|
|
||||||
|
if (extractedEnv) {
|
||||||
|
extractedEnv.forEach((exEnv: string) => {
|
||||||
|
const formattedExEnv = exEnv.slice(2, -2)
|
||||||
|
let itemLocation: InspectorLocation
|
||||||
|
if (locations.type === "header") {
|
||||||
|
itemLocation = {
|
||||||
|
type: "header",
|
||||||
|
position: locations.position,
|
||||||
|
index: index,
|
||||||
|
key: element,
|
||||||
|
}
|
||||||
|
} else if (locations.type === "parameter") {
|
||||||
|
itemLocation = {
|
||||||
|
type: "parameter",
|
||||||
|
position: locations.position,
|
||||||
|
index: index,
|
||||||
|
key: element,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
itemLocation = {
|
||||||
|
type: "url",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!envKeys.includes(formattedExEnv)) {
|
||||||
|
results.value.push({
|
||||||
|
id: "environment",
|
||||||
|
text: {
|
||||||
|
type: "text",
|
||||||
|
text: this.t("inspections.environment.not_found", {
|
||||||
|
environment: exEnv,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
icon: markRaw(IconPlusCircle),
|
||||||
|
action: {
|
||||||
|
text: this.t("inspections.environment.add_environment"),
|
||||||
|
apply: () => {
|
||||||
|
invokeAction("modals.environment.add", {
|
||||||
|
envName: "test",
|
||||||
|
variableName: formattedExEnv,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
severity: 3,
|
||||||
|
isApplicable: true,
|
||||||
|
locations: itemLocation,
|
||||||
|
doc: {
|
||||||
|
text: this.t("action.learn_more"),
|
||||||
|
link: "https://docs.hoppscotch.io/",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the inspector results for the request
|
||||||
|
* It checks if any env is used in the request ie, url, headers, params
|
||||||
|
* and checks if the env is defined in the environment using the validateEnvironmentVariables function
|
||||||
|
* @param req The request to inspect
|
||||||
|
* @returns The inspector results
|
||||||
|
*/
|
||||||
|
getInspectorFor(req: HoppRESTRequest): InspectorResult[] {
|
||||||
|
const results = ref<InspectorResult[]>([])
|
||||||
|
|
||||||
|
const headers = req.headers
|
||||||
|
|
||||||
|
const params = req.params
|
||||||
|
|
||||||
|
this.validateEnvironmentVariables([req.endpoint], results, {
|
||||||
|
type: "url",
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerKeys = Object.values(headers).map((header) => header.key)
|
||||||
|
|
||||||
|
this.validateEnvironmentVariables(headerKeys, results, {
|
||||||
|
type: "header",
|
||||||
|
position: "key",
|
||||||
|
})
|
||||||
|
|
||||||
|
const headerValues = Object.values(headers).map((header) => header.value)
|
||||||
|
|
||||||
|
this.validateEnvironmentVariables(headerValues, results, {
|
||||||
|
type: "header",
|
||||||
|
position: "value",
|
||||||
|
})
|
||||||
|
|
||||||
|
const paramsKeys = Object.values(params).map((param) => param.key)
|
||||||
|
|
||||||
|
this.validateEnvironmentVariables(paramsKeys, results, {
|
||||||
|
type: "parameter",
|
||||||
|
position: "key",
|
||||||
|
})
|
||||||
|
|
||||||
|
const paramsValues = Object.values(params).map((param) => param.value)
|
||||||
|
|
||||||
|
this.validateEnvironmentVariables(paramsValues, results, {
|
||||||
|
type: "parameter",
|
||||||
|
position: "value",
|
||||||
|
})
|
||||||
|
|
||||||
|
return results.value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { Service } from "dioc"
|
||||||
|
import { InspectionService, Inspector, InspectorResult } from ".."
|
||||||
|
import { getI18n } from "~/modules/i18n"
|
||||||
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { markRaw, ref } from "vue"
|
||||||
|
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This inspector is responsible for inspecting the header of a request.
|
||||||
|
* It checks if the header contains cookies.
|
||||||
|
*
|
||||||
|
* NOTE: Initializing this service registers it as a inspector with the Inspection Service.
|
||||||
|
*/
|
||||||
|
export class HeaderInspectorService extends Service implements Inspector {
|
||||||
|
public static readonly ID = "HEADER_INSPECTOR_SERVICE"
|
||||||
|
|
||||||
|
private t = getI18n()
|
||||||
|
|
||||||
|
public readonly inspectorID = "header"
|
||||||
|
|
||||||
|
private readonly inspection = this.bind(InspectionService)
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.inspection.registerInspector(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the header contains cookies
|
||||||
|
* @param req The request to inspect
|
||||||
|
* @returns The inspector results
|
||||||
|
*/
|
||||||
|
getInspectorFor(req: HoppRESTRequest): InspectorResult[] {
|
||||||
|
const results = ref<InspectorResult[]>([])
|
||||||
|
|
||||||
|
const cookiesCheck = (headerKey: string) => {
|
||||||
|
const cookieKeywords = ["Cookie", "Set-Cookie", "Cookie2", "Set-Cookie2"]
|
||||||
|
|
||||||
|
return cookieKeywords.includes(headerKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = req.headers
|
||||||
|
|
||||||
|
const headerKeys = Object.values(headers).map((header) => header.key)
|
||||||
|
|
||||||
|
const isContainCookies = headerKeys.includes("Cookie")
|
||||||
|
|
||||||
|
if (isContainCookies) {
|
||||||
|
headerKeys.forEach((headerKey, index) => {
|
||||||
|
if (cookiesCheck(headerKey)) {
|
||||||
|
results.value.push({
|
||||||
|
id: "header",
|
||||||
|
icon: markRaw(IconAlertTriangle),
|
||||||
|
text: {
|
||||||
|
type: "text",
|
||||||
|
text: this.t("inspections.header.cookie"),
|
||||||
|
},
|
||||||
|
severity: 2,
|
||||||
|
isApplicable: true,
|
||||||
|
locations: {
|
||||||
|
type: "header",
|
||||||
|
position: "key",
|
||||||
|
key: headerKey,
|
||||||
|
index: index,
|
||||||
|
},
|
||||||
|
doc: {
|
||||||
|
text: this.t("action.learn_more"),
|
||||||
|
link: "https://docs.hoppscotch.io/",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Service } from "dioc"
|
||||||
|
import { InspectionService, Inspector, InspectorResult } from ".."
|
||||||
|
import { getI18n } from "~/modules/i18n"
|
||||||
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { markRaw, ref } from "vue"
|
||||||
|
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||||
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This inspector is responsible for inspecting the response of a request.
|
||||||
|
* It checks if the response is successful and if it contains errors.
|
||||||
|
*
|
||||||
|
* NOTE: Initializing this service registers it as a inspector with the Inspection Service.
|
||||||
|
*/
|
||||||
|
export class ResponseInspectorService extends Service implements Inspector {
|
||||||
|
public static readonly ID = "RESPONSE_INSPECTOR_SERVICE"
|
||||||
|
|
||||||
|
private t = getI18n()
|
||||||
|
|
||||||
|
public readonly inspectorID = "response"
|
||||||
|
|
||||||
|
private readonly inspection = this.bind(InspectionService)
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.inspection.registerInspector(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
getInspectorFor(
|
||||||
|
req: HoppRESTRequest,
|
||||||
|
res: HoppRESTResponse | undefined
|
||||||
|
): InspectorResult[] {
|
||||||
|
const results = ref<InspectorResult[]>([])
|
||||||
|
if (!res) return results.value
|
||||||
|
|
||||||
|
const hasErrors = res && (res.type !== "success" || res.statusCode !== 200)
|
||||||
|
|
||||||
|
let text
|
||||||
|
|
||||||
|
if (res.type === "network_fail") {
|
||||||
|
text = this.t("inspections.response.network_error")
|
||||||
|
} else if (res.type === "fail") {
|
||||||
|
text = this.t("inspections.response.default_error")
|
||||||
|
} else if (res.type === "success" && res.statusCode === 404) {
|
||||||
|
text = this.t("inspections.response.404_error")
|
||||||
|
} else if (res.type === "success" && res.statusCode === 401) {
|
||||||
|
text = this.t("inspections.response.401_error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasErrors && text) {
|
||||||
|
results.value.push({
|
||||||
|
id: "url",
|
||||||
|
icon: markRaw(IconAlertTriangle),
|
||||||
|
text: {
|
||||||
|
type: "text",
|
||||||
|
text: text,
|
||||||
|
},
|
||||||
|
severity: 2,
|
||||||
|
isApplicable: true,
|
||||||
|
locations: {
|
||||||
|
type: "response",
|
||||||
|
},
|
||||||
|
doc: {
|
||||||
|
text: this.t("action.learn_more"),
|
||||||
|
link: "https://docs.hoppscotch.io/",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { Service } from "dioc"
|
||||||
|
import { InspectionService, Inspector, InspectorResult } from ".."
|
||||||
|
import { getI18n } from "~/modules/i18n"
|
||||||
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { computed, markRaw, ref } from "vue"
|
||||||
|
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||||
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
import { extensionStatus$ } from "~/newstore/HoppExtension"
|
||||||
|
import { useSetting } from "~/composables/settings"
|
||||||
|
import { applySetting, toggleSetting } from "~/newstore/settings"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This inspector is responsible for inspecting the URL of a request.
|
||||||
|
* It checks if the URL contains localhost and if the extension is installed.
|
||||||
|
* It also provides an action to enable the extension.
|
||||||
|
*
|
||||||
|
* NOTE: Initializing this service registers it as a inspector with the Inspection Service.
|
||||||
|
*/
|
||||||
|
export class URLInspectorService extends Service implements Inspector {
|
||||||
|
public static readonly ID = "URL_INSPECTOR_SERVICE"
|
||||||
|
|
||||||
|
private t = getI18n()
|
||||||
|
|
||||||
|
public readonly inspectorID = "url"
|
||||||
|
|
||||||
|
private readonly inspection = this.bind(InspectionService)
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.inspection.registerInspector(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
getInspectorFor(req: HoppRESTRequest): InspectorResult[] {
|
||||||
|
const PROXY_ENABLED = useSetting("PROXY_ENABLED")
|
||||||
|
|
||||||
|
const currentExtensionStatus = useReadonlyStream(extensionStatus$, null)
|
||||||
|
|
||||||
|
const isExtensionInstalled = computed(() => {
|
||||||
|
return currentExtensionStatus.value === "available"
|
||||||
|
})
|
||||||
|
const EXTENSIONS_ENABLED = useSetting("EXTENSIONS_ENABLED")
|
||||||
|
|
||||||
|
const results = ref<InspectorResult[]>([])
|
||||||
|
|
||||||
|
const url = req.endpoint
|
||||||
|
|
||||||
|
const isContainLocalhost = url.includes("localhost")
|
||||||
|
|
||||||
|
if (
|
||||||
|
isContainLocalhost &&
|
||||||
|
(!EXTENSIONS_ENABLED.value || !isExtensionInstalled.value)
|
||||||
|
) {
|
||||||
|
let text
|
||||||
|
|
||||||
|
if (!isExtensionInstalled.value) {
|
||||||
|
if (currentExtensionStatus.value === "unknown-origin") {
|
||||||
|
text = this.t("inspections.url.extension_unknown_origin")
|
||||||
|
} else {
|
||||||
|
text = this.t("inspections.url.extension_not_installed")
|
||||||
|
}
|
||||||
|
} else if (!EXTENSIONS_ENABLED.value) {
|
||||||
|
text = this.t("inspections.url.extention_not_enabled")
|
||||||
|
} else {
|
||||||
|
text = this.t("inspections.url.localhost")
|
||||||
|
}
|
||||||
|
|
||||||
|
results.value.push({
|
||||||
|
id: "url",
|
||||||
|
icon: markRaw(IconAlertTriangle),
|
||||||
|
text: {
|
||||||
|
type: "text",
|
||||||
|
text: text,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
text: this.t("inspections.url.extention_enable_action"),
|
||||||
|
apply: () => {
|
||||||
|
applySetting("EXTENSIONS_ENABLED", true)
|
||||||
|
if (PROXY_ENABLED.value) toggleSetting("PROXY_ENABLED")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
severity: 2,
|
||||||
|
isApplicable: true,
|
||||||
|
locations: {
|
||||||
|
type: "url",
|
||||||
|
},
|
||||||
|
doc: {
|
||||||
|
text: this.t("action.learn_more"),
|
||||||
|
link: "https://docs.hoppscotch.io/",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.value
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user