fix: inspections bugs (#3277)

* fix: environment add bug in inspection

* chore: add 127.0.0.1 in url inspection

* chore: update browserextension inspection help url

* fix: team env not showing bug in selector

* chore: rework inspector systems to be reactive

* chore: handling tab changes gracefully

* refactor: move out url interceptor from the platform

* chore: add view function in inspector service to get views into the list

* fix: interceptors not kicking in on initial load

* fix: don't show no internet connection error unless browser deems so

* chore: fix tests

---------

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
Nivedin
2023-08-28 17:43:46 +05:30
committed by GitHub
parent fd162e242c
commit 4adac4af38
25 changed files with 945 additions and 588 deletions

View File

@@ -21,7 +21,12 @@
<label for="value" class="font-semibold min-w-10">{{
t("environment.value")
}}</label>
<input type="text" :value="value" class="input" />
<input
v-model="editingValue"
type="text"
class="input"
:placeholder="t('environment.value')"
/>
</div>
<div class="flex items-center space-x-8 ml-2">
<label for="scope" class="font-semibold min-w-10">
@@ -105,9 +110,12 @@ watch(
scope.value = {
type: "global",
}
editingName.value = ""
replaceWithVariable.value = false
editingName.value = ""
editingValue.value = ""
}
editingName.value = props.name
editingValue.value = props.value
}
)
@@ -132,6 +140,7 @@ const scope = ref<Scope>({
const replaceWithVariable = ref(false)
const editingName = ref(props.name)
const editingValue = ref(props.value)
const addEnvironment = async () => {
if (!editingName.value) {
@@ -141,13 +150,13 @@ const addEnvironment = async () => {
if (scope.value.type === "global") {
addGlobalEnvVariable({
key: editingName.value,
value: props.value,
value: editingValue.value,
})
toast.success(`${t("environment.updated")}`)
} else if (scope.value.type === "my-environment") {
addEnvironmentVariable(scope.value.index, {
key: editingName.value,
value: props.value,
value: editingValue.value,
})
toast.success(`${t("environment.updated")}`)
} else {
@@ -155,7 +164,7 @@ const addEnvironment = async () => {
...scope.value.environment.environment.variables,
{
key: editingName.value,
value: props.value,
value: editingValue.value,
},
]
await pipe(
@@ -182,7 +191,7 @@ const addEnvironment = async () => {
//replace the currenttab endpoint containing the value in the text with variablename
currentActiveTab.value.document.request.endpoint =
currentActiveTab.value.document.request.endpoint.replace(
props.value,
editingValue.value,
variableName
)
}

View File

@@ -32,6 +32,7 @@
@keyup.escape="hide()"
>
<HoppSmartItem
v-if="!isScopeSelector"
:label="`${t('environment.no_environment')}`"
:info-icon="
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
@@ -48,6 +49,21 @@
}
"
/>
<HoppSmartItem
v-else-if="isScopeSelector && modelValue"
:label="t('environment.global')"
:icon="IconGlobe"
:info-icon="modelValue.type === 'global' ? IconCheck : undefined"
:active-info-icon="modelValue.type === 'global'"
@click="
() => {
$emit('update:modelValue', {
type: 'global',
})
hide()
}
"
/>
<HoppSmartTabs
v-model="selectedEnvTab"
:styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary ${
@@ -66,14 +82,14 @@
:key="`gen-${index}`"
:icon="IconLayers"
:label="gen.name"
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
:active-info-icon="index === selectedEnv.index"
:info-icon="isEnvActive(index) ? IconCheck : undefined"
:active-info-icon="isEnvActive(index)"
@click="
() => {
selectedEnvironmentIndex = {
type: 'MY_ENV',
index: index,
}
handleEnvironmentChange(index, {
type: 'my-environment',
environment: gen,
})
hide()
}
"
@@ -113,18 +129,14 @@
:key="`gen-team-${index}`"
:icon="IconLayers"
:label="gen.environment.name"
:info-icon="
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
"
:active-info-icon="gen.id === selectedEnv.teamEnvID"
:info-icon="isEnvActive(gen.id) ? IconCheck : undefined"
:active-info-icon="isEnvActive(gen.id)"
@click="
() => {
selectedEnvironmentIndex = {
type: 'TEAM_ENV',
teamEnvID: gen.id,
teamID: gen.teamID,
environment: gen.environment,
}
handleEnvironmentChange(index, {
type: 'team-environment',
environment: gen,
})
hide()
}
"
@@ -285,6 +297,7 @@ import IconCheck from "~icons/lucide/check"
import IconLayers from "~icons/lucide/layers"
import IconEye from "~icons/lucide/eye"
import IconEdit from "~icons/lucide/edit"
import IconGlobe from "~icons/lucide/globe"
import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n"
import { GQLError } from "~/helpers/backend/GQLClient"
@@ -295,11 +308,39 @@ import {
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import { workspaceStatus$ } from "~/newstore/workspace"
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { useColorMode } from "@composables/theming"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { invokeAction } from "~/helpers/actions"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { Environment } from "@hoppscotch/data"
import { onMounted } from "vue"
import { onLoggedIn } from "~/composables/auth"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useLocalState } from "~/newstore/localstate"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
type Scope =
| {
type: "global"
}
| {
type: "my-environment"
environment: Environment
index: number
}
| {
type: "team-environment"
environment: TeamEnvironment
}
const props = defineProps<{
isScopeSelector?: boolean
modelValue?: Scope
}>()
const emit = defineEmits<{
(e: "update:modelValue", data: Scope): void
}>()
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greater("md")
@@ -314,6 +355,38 @@ const myEnvironments = useReadonlyStream(environments$, [])
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
// TeamList-Adapter
const teamListAdapter = new TeamListAdapter(true)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const teamListFetched = ref(false)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
onLoggedIn(() => {
!teamListAdapter.isInitialized && teamListAdapter.initialize()
})
const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
REMEMBERED_TEAM_ID.value = team.id
changeWorkspace({
teamID: team.id,
teamName: team.name,
type: "team",
})
}
watch(
() => myTeams.value,
(newTeams) => {
if (newTeams && !teamListFetched.value) {
teamListFetched.value = true
if (REMEMBERED_TEAM_ID.value) {
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
if (team) switchToTeamWorkspace(team)
}
}
}
)
// TeamEnv List Adapter
const teamEnvListAdapter = new TeamEnvironmentAdapter(undefined)
const teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false)
const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null)
@@ -322,6 +395,70 @@ const teamEnvironmentList = useReadonlyStream(
[]
)
const handleEnvironmentChange = (
index: number,
env?:
| {
type: "my-environment"
environment: Environment
}
| {
type: "team-environment"
environment: TeamEnvironment
}
) => {
if (props.isScopeSelector && env) {
if (env.type === "my-environment") {
emit("update:modelValue", {
type: "my-environment",
environment: env.environment,
index,
})
} else if (env.type === "team-environment") {
emit("update:modelValue", {
type: "team-environment",
environment: env.environment,
})
}
} else {
if (env && env.type === "my-environment") {
selectedEnvironmentIndex.value = {
type: "MY_ENV",
index,
}
} else if (env && env.type === "team-environment") {
selectedEnvironmentIndex.value = {
type: "TEAM_ENV",
teamEnvID: env.environment.id,
teamID: env.environment.teamID,
environment: env.environment.environment,
}
}
}
}
const isEnvActive = (id: string | number) => {
if (props.isScopeSelector) {
if (props.modelValue?.type === "my-environment") {
return props.modelValue.index === id
} else if (props.modelValue?.type === "team-environment") {
return (
props.modelValue?.type === "team-environment" &&
props.modelValue.environment &&
props.modelValue.environment.id === id
)
}
} else {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return selectedEnv.value.index === id
} else {
return (
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnv.value.teamEnvID === id
)
}
}
}
const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$,
{ type: "NO_ENV_SELECTED" },
@@ -349,34 +486,90 @@ watch(
)
const selectedEnv = computed(() => {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
const environment =
myEnvironments.value[selectedEnvironmentIndex.value.index]
return {
type: "MY_ENV",
index: selectedEnvironmentIndex.value.index,
name: environment.name,
variables: environment.variables,
}
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = teamEnvironmentList.value.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID)
)
if (teamEnv) {
if (props.isScopeSelector) {
if (props.modelValue?.type === "my-environment") {
return {
type: "MY_ENV",
index: props.modelValue.index,
name: props.modelValue.environment?.name,
}
} else if (props.modelValue?.type === "team-environment") {
return {
type: "TEAM_ENV",
name: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
variables: teamEnv.environment.variables,
name: props.modelValue.environment.environment.name,
teamEnvID: props.modelValue.environment.id,
}
} else {
return { type: "global", name: "Global" }
}
} else {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
const environment =
myEnvironments.value[selectedEnvironmentIndex.value.index]
return {
type: "MY_ENV",
index: selectedEnvironmentIndex.value.index,
name: environment.name,
variables: environment.variables,
}
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = teamEnvironmentList.value.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID)
)
if (teamEnv) {
return {
type: "TEAM_ENV",
name: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
variables: teamEnv.environment.variables,
}
} else {
return { type: "NO_ENV_SELECTED" }
}
} else {
return { type: "NO_ENV_SELECTED" }
}
} else {
return { type: "NO_ENV_SELECTED" }
}
})
// Set the selected environment as initial scope value
onMounted(() => {
if (props.isScopeSelector) {
if (
selectedEnvironmentIndex.value.type === "MY_ENV" &&
selectedEnvironmentIndex.value.index !== undefined
) {
emit("update:modelValue", {
type: "my-environment",
environment: myEnvironments.value[selectedEnvironmentIndex.value.index],
index: selectedEnvironmentIndex.value.index,
})
} else if (
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID &&
teamEnvironmentList.value &&
teamEnvironmentList.value.length > 0
) {
const teamEnv = teamEnvironmentList.value.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID)
)
if (teamEnv) {
emit("update:modelValue", {
type: "team-environment",
environment: teamEnv,
})
}
} else {
emit("update:modelValue", {
type: "global",
})
}
}
})

View File

@@ -300,7 +300,7 @@ watch(
defineActionHandler("modals.environment.add", ({ envName, variableName }) => {
editingVariableName.value = envName
editingVariableValue.value = variableName
if (variableName) editingVariableValue.value = variableName
displayModalNew(true)
})
</script>

View File

@@ -508,30 +508,17 @@ const changeTab = (tab: ComputedHeader["source"]) => {
const inspectionService = useService(InspectionService)
const allTabResults = inspectionService.tabs
const headerKeyResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) =>
result.locations.type === "header" && result.locations.position === "key"
)
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 headerValueResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) =>
result.locations.type === "header" && result.locations.position === "value"
)
const getInspectorResult = (results: InspectorResult[], index: number) => {
return results.filter((result) => {

View File

@@ -178,7 +178,7 @@ import IconCheckCircle from "~icons/lucide/check-circle"
import IconCircle from "~icons/lucide/circle"
import IconTrash from "~icons/lucide/trash"
import IconWrapText from "~icons/lucide/wrap-text"
import { computed, reactive, ref, watch } from "vue"
import { reactive, ref, watch } from "vue"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
@@ -409,30 +409,18 @@ const clearContent = () => {
const inspectionService = useService(InspectionService)
const allTabResults = inspectionService.tabs
const parameterKeyResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) =>
result.locations.type === "parameter" && result.locations.position === "key"
)
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 parameterValueResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) =>
result.locations.type === "parameter" &&
result.locations.position === "value"
)
const getInspectorResult = (results: InspectorResult[], index: number) => {
return results.filter((result) => {

View File

@@ -642,9 +642,5 @@ const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
const inspectionService = useService(InspectionService)
const allTabResults = inspectionService.tabs
const tabResults = computed(() => {
return allTabResults.value.get(currentTabID.value) ?? []
})
const tabResults = inspectionService.getResultViewFor(currentTabID.value)
</script>

View File

@@ -145,13 +145,8 @@ const statusCategory = computed(() => {
const inspectionService = useService(InspectionService)
const allTabResults = inspectionService.tabs
const tabResults = computed(() => {
return (
allTabResults.value
.get(currentTabID.value)
?.filter((result) => result.locations.type === "response") ?? []
)
})
const tabResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) => result.locations.type === "response"
)
</script>

View File

@@ -7,6 +7,7 @@ import { HoppRESTResponse } from "../types/HoppRESTResponse"
import { getDefaultRESTRequest } from "./default"
import { HoppTestResult } from "../types/HoppTestResult"
import { platform } from "~/platform"
import { nextTick } from "vue"
export type HoppRESTTab = {
id: string
@@ -178,7 +179,9 @@ export function closeTab(tabID: string) {
tabOrdering.value.splice(tabOrdering.value.indexOf(tabID), 1)
tabMap.delete(tabID)
nextTick(() => {
tabMap.delete(tabID)
})
}
export function closeOtherTabs(tabID: string) {

View File

@@ -94,7 +94,7 @@
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, onBeforeMount, watch } from "vue"
import { ref, onMounted, onBeforeUnmount, onBeforeMount } from "vue"
import { safelyExtractRESTRequest } from "@hoppscotch/data"
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
import { useRoute } from "vue-router"
@@ -140,7 +140,6 @@ 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)
@@ -215,6 +214,8 @@ const sortTabs = (e: { oldIndex: number; newIndex: number }) => {
updateTabOrdering(e.oldIndex, e.newIndex)
}
const inspectionService = useService(InspectionService)
const removeTab = (tabID: string) => {
const tabState = getTabRef(tabID).value
@@ -465,17 +466,10 @@ defineActionHandler("request.duplicate-tab", ({ tabID }) => {
duplicateTab(tabID)
})
const inspectionService = useService(InspectionService)
useService(HeaderInspectorService)
useService(EnvironmentInspectorService)
useService(URLInspectorService)
useService(ResponseInspectorService)
watch(
() => currentTabID.value,
() => {
inspectionService.initializeTabInspectors()
},
{ immediate: true }
)
for (const inspectorDef of platform.additionalInspectors ?? []) {
useService(inspectorDef.service)
}
</script>

View File

@@ -8,6 +8,7 @@ import { TabStatePlatformDef } from "./tab"
import { AnalyticsPlatformDef } from "./analytics"
import { InterceptorsPlatformDef } from "./interceptors"
import { HoppModule } from "~/modules"
import { InspectorsPlatformDef } from "./inspectors"
export type PlatformDef = {
ui?: UIPlatformDef
@@ -22,6 +23,7 @@ export type PlatformDef = {
tabState: TabStatePlatformDef
}
interceptors: InterceptorsPlatformDef
additionalInspectors?: InspectorsPlatformDef
platformFeatureFlags: {
exportAsGIST: boolean
}

View File

@@ -0,0 +1,16 @@
import { Service } from "dioc"
import { Inspector } from "~/services/inspection"
/**
* Defines an added interceptor by the platform
*/
export type PlatformInspectorsDef = {
// We are keeping this as the only mode for now
// So that if we choose to add other modes, we can do without breaking
type: "service"
service: typeof Service<unknown> & { ID: string } & {
new (): Service & Inspector
}
}
export type InspectorsPlatformDef = PlatformInspectorsDef[]

View File

@@ -1,15 +1,17 @@
import { TestContainer } from "dioc/testing"
import { describe, expect, it, vi } from "vitest"
import { URLInspectorService } from "../url.inspector"
import { InspectionService } from "../../index"
import { ExtensionInspectorService } from "../extension.inspector"
import { InspectionService } from "~/services/inspection"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { ref } from "vue"
import { ExtensionInterceptorService } from "~/platform/std/interceptors/extension"
vi.mock("~/modules/i18n", () => ({
__esModule: true,
getI18n: () => (x: string) => x,
}))
describe("URLInspectorService", () => {
describe("ExtensionInspectorService", () => {
it("registers with the inspection service upon initialization", () => {
const container = new TestContainer()
@@ -19,7 +21,7 @@ describe("URLInspectorService", () => {
registerInspector: registerInspectorFn,
})
const urlInspector = container.bind(URLInspectorService)
const urlInspector = container.bind(ExtensionInspectorService)
expect(registerInspectorFn).toHaveBeenCalledOnce()
expect(registerInspectorFn).toHaveBeenCalledWith(urlInspector)
@@ -28,55 +30,57 @@ describe("URLInspectorService", () => {
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 urlInspector = container.bind(ExtensionInspectorService)
const req = {
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://localhost:8000/api/data",
}
})
const result = urlInspector.getInspectorFor(req)
const result = urlInspector.getInspections(req)
expect(result).toContainEqual(
expect(result.value).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 = {
container.bindMock(ExtensionInterceptorService, {
extensionStatus: ref("unknown-origin" as const),
})
const urlInspector = container.bind(ExtensionInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
}
})
const result = urlInspector.getInspectorFor(req)
const result = urlInspector.getInspections(req)
expect(result).toHaveLength(0)
expect(result.value).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 = {
container.bindMock(ExtensionInterceptorService, {
extensionStatus: ref("waiting" as const),
})
const urlInspector = container.bind(ExtensionInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://localhost:8000/api/data",
}
})
const result = urlInspector.getInspectorFor(req)
const result = urlInspector.getInspections(req)
expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
expect(result.value).toHaveLength(1)
expect(result.value[0]).toMatchObject({
text: { type: "text", text: "inspections.url.extension_not_installed" },
})
})

View File

@@ -0,0 +1,107 @@
import { Service } from "dioc"
import {
InspectionService,
Inspector,
InspectorResult,
} from "~/services/inspection"
import { getI18n } from "~/modules/i18n"
import { HoppRESTRequest } from "@hoppscotch/data"
import { computed, markRaw } from "vue"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import { Ref } from "vue"
import { InterceptorService } from "~/services/interceptor.service"
import { ExtensionInterceptorService } from "~/platform/std/interceptors/extension"
/**
* 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 ExtensionInspectorService extends Service implements Inspector {
public static readonly ID = "EXTENSION_INSPECTOR_SERVICE"
private t = getI18n()
public readonly inspectorID = "extension"
private readonly interceptorService = this.bind(InterceptorService)
private readonly extensionService = this.bind(ExtensionInterceptorService)
private readonly inspection = this.bind(InspectionService)
constructor() {
super()
this.inspection.registerInspector(this)
}
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
const currentExtensionStatus = this.extensionService.extensionStatus
const isExtensionInstalled = computed(
() => currentExtensionStatus.value === "available"
)
const EXTENSIONS_ENABLED = computed(
() => this.interceptorService.currentInterceptorID.value === "extension"
)
return computed(() => {
const results: InspectorResult[] = []
const url = req.value.endpoint
const localHostURLs = ["localhost", "127.0.0.1"]
const isContainLocalhost = localHostURLs.some((host) =>
url.includes(host)
)
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.push({
id: "url",
icon: markRaw(IconAlertTriangle),
text: {
type: "text",
text: text,
},
action: {
text: this.t("inspections.url.extention_enable_action"),
apply: () => {
this.interceptorService.currentInterceptorID.value = "extension"
},
},
severity: 2,
isApplicable: true,
locations: {
type: "url",
},
doc: {
text: this.t("action.learn_more"),
link: "https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension",
},
})
}
return results
})
}
}

View File

@@ -61,7 +61,7 @@ describe("EnvironmentMenuService", () => {
expect(actionsMock.invokeAction).toHaveBeenCalledWith(
"modals.environment.add",
{
envName: "test",
envName: "",
variableName: test,
}
)

View File

@@ -42,7 +42,7 @@ export class EnvironmentMenuService extends Service implements ContextMenu {
icon: markRaw(IconPlusCircle),
action: () => {
invokeAction("modals.environment.add", {
envName: "test",
envName: "",
variableName: text,
})
},

View File

@@ -1,7 +1,9 @@
import { HoppRESTRequest } from "@hoppscotch/data"
import { refDebounced } from "@vueuse/core"
import { Service } from "dioc"
import { computed, markRaw, reactive } from "vue"
import { Component, Ref, ref, watch } from "vue"
import { currentActiveTab, currentTabID } from "~/helpers/rest/tab"
import { currentActiveTab } from "~/helpers/rest/tab"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
/**
@@ -80,15 +82,16 @@ export interface 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
* Returns the inspector results for the request.
* NOTE: The refs passed down are readonly and are debounced to avoid performance issues
* @param req The ref to the request to inspect
* @param res The ref to the response to inspect
* @returns The ref to the inspector results
*/
getInspectorFor: (
req: HoppRESTRequest,
res?: HoppRESTResponse
) => InspectorResult[]
getInspections: (
req: Readonly<Ref<HoppRESTRequest>>,
res: Readonly<Ref<HoppRESTResponse | null | undefined>>
) => Ref<InspectorResult[]>
}
/**
@@ -98,38 +101,73 @@ export interface Inspector {
export class InspectionService extends Service {
public static readonly ID = "INSPECTION_SERVICE"
private inspectors: Map<string, Inspector> = new Map()
private inspectors: Map<string, Inspector> = reactive(new Map())
public tabs: Ref<Map<string, InspectorResult[]>> = ref(new Map())
private tabs: Ref<Map<string, InspectorResult[]>> = ref(new Map())
constructor() {
super()
this.initializeListeners()
}
/**
* Registers a inspector with the inspection service
* @param inspector The inspector instance to register
*/
public registerInspector(inspector: Inspector) {
this.inspectors.set(inspector.inspectorID, inspector)
// markRaw is required here so that the inspector is not made reactive
this.inspectors.set(inspector.inspectorID, markRaw(inspector))
}
public initializeTabInspectors() {
private initializeListeners() {
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.inspectors.entries(), currentActiveTab.value.id],
() => {
const reqRef = computed(() => currentActiveTab.value.document.request)
const resRef = computed(() => currentActiveTab.value.response)
const debouncedReq = refDebounced(reqRef, 1000, { maxWait: 2000 })
const debouncedRes = refDebounced(resRef, 1000, { maxWait: 2000 })
const inspectorRefs = Array.from(this.inspectors.values()).map((x) =>
x.getInspections(debouncedReq, debouncedRes)
)
this.tabs.value.set(
currentTabID.value,
inspectors.flatMap((x) => x)
const activeInspections = computed(() =>
inspectorRefs.flatMap((x) => x!.value)
)
watch(
() => [...inspectorRefs.flatMap((x) => x!.value)],
() => {
this.tabs.value.set(
currentActiveTab.value.id,
activeInspections.value
)
},
{ immediate: true }
)
},
{ immediate: true, deep: true }
{ immediate: true, flush: "pre" }
)
}
public deleteTabInspectorResult(tabID: string) {
// TODO: Move Tabs into a service and implement this with an event instead
this.tabs.value.delete(tabID)
}
/**
* Returns a reactive view into the inspector results for a specific tab
* @param tabID The ID of the tab to get the results for
* @param filter The filter to apply to the results.
* @returns The ref into the inspector results, if the tab doesn't exist, a ref into an empty array is returned
*/
public getResultViewFor(
tabID: string,
filter: (x: InspectorResult) => boolean = () => true
) {
return computed(() => this.tabs.value.get(tabID)?.filter(filter) ?? [])
}
}

View File

@@ -3,16 +3,23 @@ import { describe, expect, it, vi } from "vitest"
import { EnvironmentInspectorService } from "../environment.inspector"
import { InspectionService } from "../../index"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { ref } from "vue"
vi.mock("~/modules/i18n", () => ({
__esModule: true,
getI18n: () => (x: string) => x,
}))
vi.mock("~/newstore/environments", () => ({
__esModule: true,
getAggregateEnvs: () => [{ key: "EXISTING_ENV_VAR", value: "test_value" }],
}))
vi.mock("~/newstore/environments", async () => {
const { BehaviorSubject }: any = await vi.importActual("rxjs")
return {
__esModule: true,
aggregateEnvs$: new BehaviorSubject([
{ key: "EXISTING_ENV_VAR", value: "test_value" },
]),
}
})
describe("EnvironmentInspectorService", () => {
it("registers with the inspection service upon initialization", () => {
@@ -35,14 +42,14 @@ describe("EnvironmentInspectorService", () => {
const container = new TestContainer()
const envInspector = container.bind(EnvironmentInspectorService)
const req = {
const req = ref({
...getDefaultRESTRequest(),
endpoint: "<<UNDEFINED_ENV_VAR>>",
}
})
const result = envInspector.getInspectorFor(req)
const result = envInspector.getInspections(req)
expect(result).toContainEqual(
expect(result.value).toContainEqual(
expect.objectContaining({
id: "environment",
isApplicable: true,
@@ -58,31 +65,31 @@ describe("EnvironmentInspectorService", () => {
const container = new TestContainer()
const envInspector = container.bind(EnvironmentInspectorService)
const req = {
const req = ref({
...getDefaultRESTRequest(),
endpoint: "<<EXISTING_ENV_VAR>>",
}
})
const result = envInspector.getInspectorFor(req)
const result = envInspector.getInspections(req)
expect(result).toHaveLength(0)
expect(result.value).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 = {
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [
{ key: "<<UNDEFINED_ENV_VAR>>", value: "some-value", active: true },
],
}
})
const result = envInspector.getInspectorFor(req)
const result = envInspector.getInspections(req)
expect(result).toContainEqual(
expect(result.value).toContainEqual(
expect.objectContaining({
id: "environment",
isApplicable: true,
@@ -98,34 +105,34 @@ describe("EnvironmentInspectorService", () => {
const container = new TestContainer()
const envInspector = container.bind(EnvironmentInspectorService)
const req = {
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
],
}
})
const result = envInspector.getInspectorFor(req)
const result = envInspector.getInspections(req)
expect(result).toHaveLength(0)
expect(result.value).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 = {
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
params: [
{ key: "<<UNDEFINED_ENV_VAR>>", value: "some-value", active: true },
],
}
})
const result = envInspector.getInspectorFor(req)
const result = envInspector.getInspections(req)
expect(result).toContainEqual(
expect(result.value).toContainEqual(
expect.objectContaining({
id: "environment",
isApplicable: true,
@@ -141,18 +148,18 @@ describe("EnvironmentInspectorService", () => {
const container = new TestContainer()
const envInspector = container.bind(EnvironmentInspectorService)
const req = {
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [],
params: [
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
],
}
})
const result = envInspector.getInspectorFor(req)
const result = envInspector.getInspections(req)
expect(result).toHaveLength(0)
expect(result.value).toHaveLength(0)
})
})
})

View File

@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest"
import { HeaderInspectorService } from "../header.inspector"
import { InspectionService } from "../../index"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { ref } from "vue"
vi.mock("~/modules/i18n", () => ({
__esModule: true,
@@ -30,15 +31,15 @@ describe("HeaderInspectorService", () => {
const container = new TestContainer()
const headerInspector = container.bind(HeaderInspectorService)
const req = {
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [{ key: "Cookie", value: "some-cookie", active: true }],
}
})
const result = headerInspector.getInspectorFor(req)
const result = headerInspector.getInspections(req)
expect(result).toContainEqual(
expect(result.value).toContainEqual(
expect.objectContaining({ id: "header", isApplicable: true })
)
})
@@ -47,15 +48,15 @@ describe("HeaderInspectorService", () => {
const container = new TestContainer()
const headerInspector = container.bind(HeaderInspectorService)
const req = {
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [{ key: "Authorization", value: "Bearer abcd", active: true }],
}
})
const result = headerInspector.getInspectorFor(req)
const result = headerInspector.getInspections(req)
expect(result).toHaveLength(0)
expect(result.value).toHaveLength(0)
})
})
})

View File

@@ -0,0 +1,245 @@
import { TestContainer } from "dioc/testing"
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"
import { ResponseInspectorService } from "../response.inspector"
import { InspectionService } from "../../index"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { ref } from "vue"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
vi.mock("~/modules/i18n", () => ({
__esModule: true,
getI18n: () => (x: string) => x,
}))
describe("ResponseInspectorService", () => {
beforeEach(() => {
vi.stubGlobal("navigator", {
onLine: true,
})
})
afterEach(() => {
vi.unstubAllGlobals()
})
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 = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const result = responseInspector.getInspections(req, ref(undefined))
expect(result.value).toHaveLength(0)
})
it("should return an inspector result when response type is not success or status code is not 200 and if the network is not available", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "network_fail",
error: new Error("test"),
req: req.value,
})
vi.stubGlobal("navigator", {
onLine: false,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).toContainEqual(
expect.objectContaining({ id: "url", isApplicable: true })
)
})
it("should return no inspector result when response type is not success or status code is not 200 and if the network is not available", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "network_fail",
error: new Error("test"),
req: req.value,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).toHaveLength(0)
})
it("should handle network_fail responses and return nothing when no network is present", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "network_fail",
error: new Error("test"),
req: req.value,
})
vi.stubGlobal("navigator", {
onLine: false,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).toContainEqual(
expect.objectContaining({
text: { type: "text", text: "inspections.response.network_error" },
})
)
})
it("should handle network_fail responses and return nothing when network is present", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "network_fail",
error: new Error("test"),
req: req.value,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).toHaveLength(0)
})
it("should handle fail responses", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "fail",
statusCode: 500,
body: Uint8Array.from([]),
headers: [],
meta: { responseDuration: 0, responseSize: 0 },
req: req.value,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).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 = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "success",
statusCode: 404,
body: Uint8Array.from([]),
headers: [],
meta: { responseDuration: 0, responseSize: 0 },
req: req.value,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).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 = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "success",
statusCode: 401,
body: Uint8Array.from([]),
headers: [],
meta: { responseDuration: 0, responseSize: 0 },
req: req.value,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).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 = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "success",
statusCode: 200,
body: Uint8Array.from([]),
headers: [],
meta: { responseDuration: 0, responseSize: 0 },
req: req.value,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).toHaveLength(0)
})
})
})

View File

@@ -1,151 +0,0 @@
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)
})
})
})

View File

@@ -6,11 +6,13 @@ import {
InspectorResult,
} from ".."
import { Service } from "dioc"
import { Ref, markRaw, ref } from "vue"
import { Ref, markRaw } from "vue"
import IconPlusCircle from "~icons/lucide/plus-circle"
import { HoppRESTRequest } from "@hoppscotch/data"
import { getAggregateEnvs } from "~/newstore/environments"
import { aggregateEnvs$ } from "~/newstore/environments"
import { invokeAction } from "~/helpers/actions"
import { computed } from "vue"
import { useStreamStatic } from "~/composables/stream"
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
@@ -34,6 +36,10 @@ export class EnvironmentInspectorService extends Service implements Inspector {
private readonly inspection = this.bind(InspectionService)
private aggregateEnvs = useStreamStatic(aggregateEnvs$, [], () => {
/* noop */
})[0]
constructor() {
super()
@@ -49,11 +55,11 @@ export class EnvironmentInspectorService extends Service implements Inspector {
*/
private validateEnvironmentVariables = (
target: any[],
results: Ref<InspectorResult[]>,
locations: InspectorLocation
) => {
const env = getAggregateEnvs()
const envKeys = env.map((e) => e.key)
const newErrors: InspectorResult[] = []
const envKeys = this.aggregateEnvs.value.map((e) => e.key)
target.forEach((element, index) => {
if (isENVInString(element)) {
@@ -83,7 +89,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
}
}
if (!envKeys.includes(formattedExEnv)) {
results.value.push({
newErrors.push({
id: "environment",
text: {
type: "text",
@@ -96,8 +102,8 @@ export class EnvironmentInspectorService extends Service implements Inspector {
text: this.t("inspections.environment.add_environment"),
apply: () => {
invokeAction("modals.environment.add", {
envName: "test",
variableName: formattedExEnv,
envName: formattedExEnv,
variableName: "",
})
},
},
@@ -114,54 +120,61 @@ export class EnvironmentInspectorService extends Service implements Inspector {
}
}
})
return newErrors
}
/**
* 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[]>([])
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
return computed(() => {
const results: InspectorResult[] = []
const headers = req.headers
const headers = req.value.headers
const params = req.params
const params = req.value.params
this.validateEnvironmentVariables([req.endpoint], results, {
type: "url",
results.push(
...this.validateEnvironmentVariables([req.value.endpoint], {
type: "url",
})
)
const headerKeys = Object.values(headers).map((header) => header.key)
results.push(
...this.validateEnvironmentVariables(headerKeys, {
type: "header",
position: "key",
})
)
const headerValues = Object.values(headers).map((header) => header.value)
results.push(
...this.validateEnvironmentVariables(headerValues, {
type: "header",
position: "value",
})
)
const paramsKeys = Object.values(params).map((param) => param.key)
results.push(
...this.validateEnvironmentVariables(paramsKeys, {
type: "parameter",
position: "key",
})
)
const paramsValues = Object.values(params).map((param) => param.value)
results.push(
...this.validateEnvironmentVariables(paramsValues, {
type: "parameter",
position: "value",
})
)
return results
})
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
}
}

View File

@@ -2,7 +2,7 @@ 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 { Ref, computed, markRaw } from "vue"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
/**
@@ -26,53 +26,50 @@ export class HeaderInspectorService extends Service implements Inspector {
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[]>([])
private cookiesCheck(headerKey: string) {
const cookieKeywords = ["Cookie", "Set-Cookie", "Cookie2", "Set-Cookie2"]
const cookiesCheck = (headerKey: string) => {
const cookieKeywords = ["Cookie", "Set-Cookie", "Cookie2", "Set-Cookie2"]
return cookieKeywords.includes(headerKey)
}
return cookieKeywords.includes(headerKey)
}
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
return computed(() => {
const results: InspectorResult[] = []
const headers = req.headers
const headers = req.value.headers
const headerKeys = Object.values(headers).map((header) => header.key)
const headerKeys = Object.values(headers).map((header) => header.key)
const isContainCookies = headerKeys.includes("Cookie")
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/",
},
})
}
})
}
if (isContainCookies) {
headerKeys.forEach((headerKey, index) => {
if (this.cookiesCheck(headerKey)) {
results.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
return results
})
}
}

View File

@@ -2,9 +2,11 @@ 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 { markRaw } from "vue"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { Ref } from "vue"
import { computed } from "vue"
/**
* This inspector is responsible for inspecting the response of a request.
@@ -27,47 +29,50 @@ export class ResponseInspectorService extends Service implements Inspector {
this.inspection.registerInspector(this)
}
getInspectorFor(
req: HoppRESTRequest,
res: HoppRESTResponse | undefined
): InspectorResult[] {
const results = ref<InspectorResult[]>([])
if (!res) return results.value
getInspections(
_req: Readonly<Ref<HoppRESTRequest>>,
res: Readonly<Ref<HoppRESTResponse | null | undefined>>
) {
return computed(() => {
const results: InspectorResult[] = []
if (!res.value) return results
const hasErrors = res && (res.type !== "success" || res.statusCode !== 200)
const hasErrors =
res && (res.value.type !== "success" || res.value.statusCode !== 200)
let text
let text: string | undefined = undefined
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 (res.value.type === "network_fail" && !navigator.onLine) {
text = this.t("inspections.response.network_error")
} else if (res.value.type === "fail") {
text = this.t("inspections.response.default_error")
} else if (res.value.type === "success" && res.value.statusCode === 404) {
text = this.t("inspections.response.404_error")
} else if (res.value.type === "success" && res.value.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/",
},
})
}
if (hasErrors && text) {
results.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
return results
})
}
}

View File

@@ -1,96 +0,0 @@
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
}
}

View File

@@ -7,6 +7,7 @@ import { def as historyDef } from "./platform/history/history.platform"
import { def as tabStateDef } from "./platform/tabState/tabState.platform"
import { browserInterceptor } from "@hoppscotch/common/platform/std/interceptors/browser"
import { proxyInterceptor } from "@hoppscotch/common/platform/std/interceptors/proxy"
import { ExtensionInspectorService } from "@hoppscotch/common/platform/std/inspections/extension.inspector"
import { ExtensionInterceptorService } from "@hoppscotch/common/platform/std/interceptors/extension"
import { stdFooterItems } from "@hoppscotch/common/platform/std/ui/footerItem"
import { stdSupportOptionItems } from "@hoppscotch/common/platform/std/ui/supportOptionsItem"
@@ -32,6 +33,9 @@ createHoppApp("#app", {
{ type: "service", service: ExtensionInterceptorService },
],
},
additionalInspectors: [
{ type: "service", service: ExtensionInspectorService },
],
platformFeatureFlags: {
exportAsGIST: false,
},