From 77af577778c8437ca95cb9d8fadd59a3745c0809 Mon Sep 17 00:00:00 2001 From: Anwarul Islam Date: Tue, 25 Nov 2025 20:04:54 +0600 Subject: [PATCH] feat: mock server feature enhancements (#5609) Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com> --- packages/hoppscotch-common/locales/en.json | 15 +- .../hoppscotch-common/src/components.d.ts | 7 + .../components/collections/ImportExport.vue | 51 +- .../collections/graphql/ImportExport.vue | 25 +- .../src/components/collections/index.vue | 2 +- .../mockServer/ConfigureMockServerModal.vue | 313 ++++++++++ .../mockServer/CreateMockServer.vue | 543 ----------------- .../mockServer/CreateNewMockServerModal.vue | 548 ++++++++++++++++++ .../mockServer/MockServerCreatedInfo.vue | 93 +++ .../mockServer/MockServerDashboard.vue | 8 +- .../src/composables/useMockServer.ts | 302 ++++++++++ .../CreateRESTRootUserCollection.graphql | 8 + .../mutations/CreateRESTUserRequest.graphql | 15 + .../src/helpers/collection/collection.ts | 28 + .../helpers/mockServer/exampleCollection.ts | 327 +++++++++++ .../mockServer/exampleMockCollection.ts | 343 +++++++++++ 16 files changed, 2005 insertions(+), 623 deletions(-) create mode 100644 packages/hoppscotch-common/src/components/mockServer/ConfigureMockServerModal.vue delete mode 100644 packages/hoppscotch-common/src/components/mockServer/CreateMockServer.vue create mode 100644 packages/hoppscotch-common/src/components/mockServer/CreateNewMockServerModal.vue create mode 100644 packages/hoppscotch-common/src/components/mockServer/MockServerCreatedInfo.vue create mode 100644 packages/hoppscotch-common/src/composables/useMockServer.ts create mode 100644 packages/hoppscotch-common/src/helpers/backend/gql/mutations/CreateRESTRootUserCollection.graphql create mode 100644 packages/hoppscotch-common/src/helpers/backend/gql/mutations/CreateRESTUserRequest.graphql create mode 100644 packages/hoppscotch-common/src/helpers/mockServer/exampleCollection.ts create mode 100644 packages/hoppscotch-common/src/helpers/mockServer/exampleMockCollection.ts diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index c14f3b4d..d70c60f0 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -1062,7 +1062,20 @@ "response_headers": "Response Headers", "response_body": "Response Body", "log_deleted": "Log deleted successfully", - "description": "Mock servers allow you to simulate API responses based on your collection's example responses." + "description": "Mock servers allow you to simulate API responses based on your collection's example responses.", + "set_in_environment": "Set in environment", + "set_in_environment_hint": "The mock server URL will be automatically added as 'mockUrl' variable in the collection's environment", + "environment_variable_added": "Mock URL added to environment", + "environment_variable_updated": "Mock URL updated in environment", + "environment_created_with_variable": "Environment created with mock URL", + "create_example_collection": "Create example collection", + "create_example_collection_hint": "Create a pet store example collection with sample requests (GET, POST, PUT, DELETE)", + "creating_example_collection": "Creating example collection...", + "failed_to_create_collection": "Failed to create example collection", + "enable_example_collection_hint": "Please enable 'Create example collection' toggle for new collection mode", + "new_collection_name_hint": "The collection will be created with the same name as your mock server", + "existing_collection": "Existing Collection", + "new_collection": "New Collection" }, "preRequest": { "javascript_code": "JavaScript Code", diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts index e4083cbb..1290b92d 100644 --- a/packages/hoppscotch-common/src/components.d.ts +++ b/packages/hoppscotch-common/src/components.d.ts @@ -241,15 +241,19 @@ declare module 'vue' { IconLucideCode2: typeof import('~icons/lucide/code2')['default'] IconLucideEyeOff: typeof import('~icons/lucide/eye-off')['default'] IconLucideFileQuestion: typeof import('~icons/lucide/file-question')['default'] + IconLucideFileText: typeof import('~icons/lucide/file-text')['default'] IconLucideFolder: typeof import('~icons/lucide/folder')['default'] IconLucideFolderOpen: typeof import('~icons/lucide/folder-open')['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'] IconLucideLightbulb: typeof import('~icons/lucide/lightbulb')['default'] IconLucideListEnd: typeof import('~icons/lucide/list-end')['default'] IconLucideLoader: typeof import('~icons/lucide/loader')['default'] IconLucideLoader2: typeof import('~icons/lucide/loader2')['default'] + IconLucideLock: typeof import('~icons/lucide/lock')['default'] IconLucideMinus: typeof import('~icons/lucide/minus')['default'] IconLucidePlusCircle: typeof import('~icons/lucide/plus-circle')['default'] IconLucideSearch: typeof import('~icons/lucide/search')['default'] @@ -285,9 +289,12 @@ declare module 'vue' { LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default'] LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default'] LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default'] + MockServerConfigureMockServerModal: typeof import('./components/mockServer/ConfigureMockServerModal.vue')['default'] MockServerCreateMockServer: typeof import('./components/mockServer/CreateMockServer.vue')['default'] + MockServerCreateNewMockServerModal: typeof import('./components/mockServer/CreateNewMockServerModal.vue')['default'] MockServerEditMockServer: typeof import('./components/mockServer/EditMockServer.vue')['default'] MockServerLogSection: typeof import('./components/mockServer/LogSection.vue')['default'] + MockServerMockServerCreatedInfo: typeof import('./components/mockServer/MockServerCreatedInfo.vue')['default'] MockServerMockServerDashboard: typeof import('./components/mockServer/MockServerDashboard.vue')['default'] MockServerMockServerLogs: typeof import('./components/mockServer/MockServerLogs.vue')['default'] MonacoScriptEditor: typeof import('./components/MonacoScriptEditor.vue')['default'] diff --git a/packages/hoppscotch-common/src/components/collections/ImportExport.vue b/packages/hoppscotch-common/src/components/collections/ImportExport.vue index 68ad9b49..e3f80a23 100644 --- a/packages/hoppscotch-common/src/components/collections/ImportExport.vue +++ b/packages/hoppscotch-common/src/components/collections/ImportExport.vue @@ -13,6 +13,7 @@ import { HoppCollection } from "@hoppscotch/data" import * as E from "fp-ts/Either" import { PropType, Ref, computed, ref } from "vue" +import { transformCollectionForImport } from "~/helpers/collection/collection" import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource" import { UrlSource } from "~/helpers/import-export/import/import-sources/UrlSource" @@ -125,7 +126,7 @@ const importToPersonalWorkspace = async (collections: HoppCollection[]) => { if (currentUser.value) { try { const transformedCollection = collections.map((collection) => - translateToPersonalCollectionFormat(collection) + transformCollectionForImport(collection) ) const res = await importUserCollectionsFromJSON( @@ -162,52 +163,6 @@ const importToPersonalWorkspace = async (collections: HoppCollection[]) => { } } -function translateToTeamCollectionFormat(x: HoppCollection) { - const folders: HoppCollection[] = (x.folders ?? []).map( - translateToTeamCollectionFormat - ) - - const data = { - auth: x.auth, - headers: x.headers, - variables: x.variables, - description: x.description, - } - - const obj = { - ...x, - folders, - data, - } - - if (x.id) obj.id = x.id - - return obj -} - -function translateToPersonalCollectionFormat(x: HoppCollection) { - const folders: HoppCollection[] = (x.folders ?? []).map( - translateToPersonalCollectionFormat - ) - - const data = { - auth: x.auth, - headers: x.headers, - variables: x.variables, - description: x.description, - } - - const obj = { - ...x, - folders, - data, - } - - if (x.id) obj.id = x.id - - return obj -} - const importToTeamsWorkspace = async (collections: HoppCollection[]) => { if (!hasTeamWriteAccess.value || !selectedTeamID.value) { return E.left({ @@ -216,7 +171,7 @@ const importToTeamsWorkspace = async (collections: HoppCollection[]) => { } const transformedCollection = collections.map((collection) => - translateToTeamCollectionFormat(collection) + transformCollectionForImport(collection) ) const res = await toTeamsImporter( diff --git a/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue b/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue index b0e63b87..58d54ff5 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue @@ -12,6 +12,7 @@ import { HoppCollection } from "@hoppscotch/data" import * as E from "fp-ts/Either" import { ref } from "vue" +import { transformCollectionForImport } from "~/helpers/collection/collection" import { useI18n } from "~/composables/i18n" import { useToast } from "~/composables/toast" @@ -242,7 +243,7 @@ const handleImportToStore = async (gqlCollections: HoppCollection[]) => { if (currentUser.value) { try { const transformedCollection = gqlCollections.map((collection) => - translateToPersonalCollectionFormat(collection) + transformCollectionForImport(collection) ) const res = await importUserCollectionsFromJSON( @@ -282,28 +283,6 @@ const handleImportToStore = async (gqlCollections: HoppCollection[]) => { } } -function translateToPersonalCollectionFormat(x: HoppCollection) { - const folders: HoppCollection[] = (x.folders ?? []).map( - translateToPersonalCollectionFormat - ) - - const data = { - auth: x.auth, - headers: x.headers, - variables: x.variables, - } - - const obj = { - ...x, - folders, - data, - } - - if (x.id) obj.id = x.id - - return obj -} - const emit = defineEmits<{ (e: "hide-modal"): () => void }>() diff --git a/packages/hoppscotch-common/src/components/collections/index.vue b/packages/hoppscotch-common/src/components/collections/index.vue index dd78827f..57731cb8 100644 --- a/packages/hoppscotch-common/src/components/collections/index.vue +++ b/packages/hoppscotch-common/src/components/collections/index.vue @@ -248,7 +248,7 @@ @hide-modal="showCollectionsRunnerModal = false" /> - + diff --git a/packages/hoppscotch-common/src/components/mockServer/ConfigureMockServerModal.vue b/packages/hoppscotch-common/src/components/mockServer/ConfigureMockServerModal.vue new file mode 100644 index 00000000..9eb3d104 --- /dev/null +++ b/packages/hoppscotch-common/src/components/mockServer/ConfigureMockServerModal.vue @@ -0,0 +1,313 @@ + + + diff --git a/packages/hoppscotch-common/src/components/mockServer/CreateMockServer.vue b/packages/hoppscotch-common/src/components/mockServer/CreateMockServer.vue deleted file mode 100644 index 37714872..00000000 --- a/packages/hoppscotch-common/src/components/mockServer/CreateMockServer.vue +++ /dev/null @@ -1,543 +0,0 @@ - - - diff --git a/packages/hoppscotch-common/src/components/mockServer/CreateNewMockServerModal.vue b/packages/hoppscotch-common/src/components/mockServer/CreateNewMockServerModal.vue new file mode 100644 index 00000000..81f0cf28 --- /dev/null +++ b/packages/hoppscotch-common/src/components/mockServer/CreateNewMockServerModal.vue @@ -0,0 +1,548 @@ + + + diff --git a/packages/hoppscotch-common/src/components/mockServer/MockServerCreatedInfo.vue b/packages/hoppscotch-common/src/components/mockServer/MockServerCreatedInfo.vue new file mode 100644 index 00000000..0416b5d1 --- /dev/null +++ b/packages/hoppscotch-common/src/components/mockServer/MockServerCreatedInfo.vue @@ -0,0 +1,93 @@ + + + diff --git a/packages/hoppscotch-common/src/components/mockServer/MockServerDashboard.vue b/packages/hoppscotch-common/src/components/mockServer/MockServerDashboard.vue index 3a0c05e1..e29ffff0 100644 --- a/packages/hoppscotch-common/src/components/mockServer/MockServerDashboard.vue +++ b/packages/hoppscotch-common/src/components/mockServer/MockServerDashboard.vue @@ -177,11 +177,7 @@ - + (null) diff --git a/packages/hoppscotch-common/src/composables/useMockServer.ts b/packages/hoppscotch-common/src/composables/useMockServer.ts new file mode 100644 index 00000000..8159250c --- /dev/null +++ b/packages/hoppscotch-common/src/composables/useMockServer.ts @@ -0,0 +1,302 @@ +import { useI18n } from "@composables/i18n" +import { useReadonlyStream } from "@composables/stream" +import { useToast } from "@composables/toast" +import { useService } from "dioc/vue" +import { pipe } from "fp-ts/function" +import * as TE from "fp-ts/TaskEither" +import { computed } from "vue" +import { MockServer, WorkspaceType } from "~/helpers/backend/graphql" +import { + createMockServer as createMockServerMutation, + updateMockServer, +} from "~/helpers/backend/mutations/MockServer" +import { + createTeamEnvironment, + updateTeamEnvironment, +} from "~/helpers/backend/mutations/TeamEnvironment" +import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter" +import { restCollections$ } from "~/newstore/collections" +import { + addEnvironmentVariable, + createEnvironment, + environments$, + getSelectedEnvironmentIndex, + updateEnvironmentVariable, +} from "~/newstore/environments" +import { + addMockServer, + mockServers$, + updateMockServer as updateMockServerInStore, +} from "~/newstore/mockServers" +import { TeamCollectionsService } from "~/services/team-collection.service" +import { WorkspaceService } from "~/services/workspace.service" + +export function useMockServer() { + const t = useI18n() + const toast = useToast() + const workspaceService = useService(WorkspaceService) + const teamCollectionsService = useService(TeamCollectionsService) + + const mockServers = useReadonlyStream(mockServers$, []) + const collections = useReadonlyStream(restCollections$, []) + const currentWorkspace = computed( + () => workspaceService.currentWorkspace.value + ) + + // Get collections based on current workspace + const availableCollections = computed(() => { + if ( + currentWorkspace.value.type === "team" && + currentWorkspace.value.teamID + ) { + return teamCollectionsService.collections.value || [] + } + return collections.value + }) + + // Environment management + const myEnvironments = useReadonlyStream(environments$, []) + const teamEnvironmentAdapter = new TeamEnvironmentAdapter( + currentWorkspace.value.type === "team" + ? currentWorkspace.value.teamID + : undefined + ) + + // Function to add mock URL to environment + const addMockUrlToEnvironment = async ( + mockUrl: string, + collectionName: string + ) => { + const workspaceType = currentWorkspace.value.type + + if (workspaceType === "personal") { + // For personal workspace, add to selected environment or create new one + const selectedEnvIndex = getSelectedEnvironmentIndex() + + if (selectedEnvIndex.type === "MY_ENV") { + // Check if mockUrl already exists in the environment + const env = myEnvironments.value[selectedEnvIndex.index] + const existingVariableIndex = env.variables.findIndex( + (v) => v.key === "mockUrl" + ) + + if (existingVariableIndex === -1) { + // Add to existing selected environment + addEnvironmentVariable(selectedEnvIndex.index, { + key: "mockUrl", + initialValue: mockUrl, + currentValue: mockUrl, + secret: false, + }) + toast.success(t("mock_server.environment_variable_added")) + } else { + // Update existing mockUrl variable with new value using the store dispatcher + updateEnvironmentVariable( + selectedEnvIndex.index, + existingVariableIndex, + { + key: "mockUrl", + initialValue: mockUrl, + currentValue: mockUrl, + } + ) + toast.success(t("mock_server.environment_variable_updated")) + } + } else { + // Create a new environment with the mock URL + const envName = `${collectionName} Environment` + createEnvironment(envName, [ + { + key: "mockUrl", + initialValue: mockUrl, + currentValue: mockUrl, + secret: false, + }, + ]) + toast.success(t("mock_server.environment_created_with_variable")) + } + } else if (workspaceType === "team" && currentWorkspace.value.teamID) { + // For team workspace, create a new team environment or update existing one + const teamID = currentWorkspace.value.teamID + + // Check if there's an existing team environment for this collection + const teamEnvs = teamEnvironmentAdapter.teamEnvironmentList$.value + const existingEnv = teamEnvs.find((env) => + env.environment.name.includes(collectionName) + ) + + if (existingEnv) { + // Update existing environment (add or update the mockUrl variable) + const existingVariableIndex = + existingEnv.environment.variables.findIndex( + (v) => v.key === "mockUrl" + ) + + let updatedVariables + let successMessage + + if (existingVariableIndex === -1) { + // Variable doesn't exist, add it + updatedVariables = [ + ...existingEnv.environment.variables, + { key: "mockUrl", value: mockUrl }, + ] + successMessage = t("mock_server.environment_variable_added") + } else { + // Variable exists, update its value + updatedVariables = existingEnv.environment.variables.map((v, idx) => + idx === existingVariableIndex ? { ...v, value: mockUrl } : v + ) + successMessage = t("mock_server.environment_variable_updated") + } + + await pipe( + updateTeamEnvironment( + JSON.stringify(updatedVariables), + existingEnv.id, + existingEnv.environment.name + ), + TE.match( + (error) => { + console.error("Failed to update team environment:", error) + toast.error(t("error.something_went_wrong")) + }, + () => { + toast.success(successMessage) + } + ) + )() + } else { + // Create new team environment + const envName = `${collectionName} Environment` + const variables = [{ key: "mockUrl", value: mockUrl }] + + await pipe( + createTeamEnvironment(JSON.stringify(variables), teamID, envName), + TE.match( + (error) => { + console.error("Failed to create team environment:", error) + toast.error(t("error.something_went_wrong")) + }, + () => { + toast.success(t("mock_server.environment_created_with_variable")) + } + ) + )() + } + } + } + + // Create new mock server + const createMockServer = async (params: { + mockServerName: string + collectionID: string + delayInMs: number + isPublic: boolean + setInEnvironment: boolean + collectionName: string + }) => { + const { + mockServerName, + collectionID, + delayInMs, + isPublic, + setInEnvironment, + collectionName, + } = params + + if (!mockServerName.trim() || !collectionID) { + if (!collectionID) { + toast.error(t("mock_server.select_collection_error")) + } + return { success: false, server: null } + } + + // Determine workspace type and ID based on current workspace + const workspaceType = + currentWorkspace.value.type === "team" + ? WorkspaceType.Team + : WorkspaceType.User + const workspaceID = + currentWorkspace.value.type === "team" + ? currentWorkspace.value.teamID + : undefined + + const result = await pipe( + createMockServerMutation( + mockServerName.trim(), + collectionID, + workspaceType, + workspaceID, + delayInMs, + isPublic + ), + TE.match( + (error) => { + toast.error(String(error) || t("error.something_went_wrong")) + return null as MockServer | null + }, + (result) => { + toast.success(t("mock_server.mock_server_created")) + // Add the new mock server to the store + addMockServer(result) + return result as MockServer + } + ) + )() + + if (!result) { + return { success: false, server: null } + } + + // Add mock URL to environment if enabled + if (setInEnvironment) { + const mockUrl = + result.serverUrlPathBased || result.serverUrlDomainBased || "" + if (mockUrl) { + await addMockUrlToEnvironment(mockUrl, collectionName) + } + } + + return { success: true, server: result } + } + + // Toggle mock server active state + const toggleMockServer = async (mockServer: MockServer) => { + const newActiveState = !mockServer.isActive + + return await pipe( + updateMockServer(mockServer.id, { isActive: newActiveState }), + TE.match( + () => { + toast.error(t("error.something_went_wrong")) + return { success: false } + }, + () => { + toast.success( + newActiveState + ? t("mock_server.server_started") + : t("mock_server.server_stopped") + ) + + // Update the mock server in the store + updateMockServerInStore(mockServer.id, { isActive: newActiveState }) + + return { success: true } + } + ) + )() + } + + return { + // State + mockServers, + availableCollections, + currentWorkspace, + + // Functions + createMockServer, + toggleMockServer, + addMockUrlToEnvironment, + } +} diff --git a/packages/hoppscotch-common/src/helpers/backend/gql/mutations/CreateRESTRootUserCollection.graphql b/packages/hoppscotch-common/src/helpers/backend/gql/mutations/CreateRESTRootUserCollection.graphql new file mode 100644 index 00000000..613c7d6b --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/backend/gql/mutations/CreateRESTRootUserCollection.graphql @@ -0,0 +1,8 @@ +mutation CreateRESTRootUserCollection($title: String!, $data: String) { + createRESTRootUserCollection(title: $title, data: $data) { + id + title + data + type + } +} diff --git a/packages/hoppscotch-common/src/helpers/backend/gql/mutations/CreateRESTUserRequest.graphql b/packages/hoppscotch-common/src/helpers/backend/gql/mutations/CreateRESTUserRequest.graphql new file mode 100644 index 00000000..de48db86 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/backend/gql/mutations/CreateRESTUserRequest.graphql @@ -0,0 +1,15 @@ +mutation CreateRESTUserRequest( + $collectionID: ID! + $title: String! + $request: String! +) { + createRESTUserRequest( + collectionID: $collectionID + title: $title + request: $request + ) { + id + title + request + } +} diff --git a/packages/hoppscotch-common/src/helpers/collection/collection.ts b/packages/hoppscotch-common/src/helpers/collection/collection.ts index e782b4b7..2656dbf7 100644 --- a/packages/hoppscotch-common/src/helpers/collection/collection.ts +++ b/packages/hoppscotch-common/src/helpers/collection/collection.ts @@ -286,3 +286,31 @@ export function getFoldersByPath( return currentCollection.folders } + +/** + * Transforms a collection to the format expected by team or personal collections. + * Extracts auth, headers, and variables into a data object and recursively processes folders. + * @param collection The collection to transform + * @returns The transformed collection + */ +export function transformCollectionForImport(collection: any): any { + const folders: any[] = (collection.folders ?? []).map( + transformCollectionForImport + ) + + const data = { + auth: collection.auth, + headers: collection.headers, + variables: collection.variables, + } + + const obj = { + ...collection, + folders, + data, + } + + if (collection.id) obj.id = collection.id + + return obj +} diff --git a/packages/hoppscotch-common/src/helpers/mockServer/exampleCollection.ts b/packages/hoppscotch-common/src/helpers/mockServer/exampleCollection.ts new file mode 100644 index 00000000..2a268076 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/mockServer/exampleCollection.ts @@ -0,0 +1,327 @@ +import { + HoppCollection, + HoppRESTRequest, + makeCollection, + makeRESTRequest, +} from "@hoppscotch/data" +import { uniqueID } from "~/helpers/utils/uniqueID" + +const MOCK_URL_VAR = "<>" + +/** + * Returns a JSON string of the Pet Store example collection for import + * @returns JSON string representation of the collection + */ +export function getPetStoreExampleJSON(): string { + const collection = createExamplePetStoreCollection() + return JSON.stringify(collection) +} + +/** + * Creates an example Pet Store collection with 4 requests (GET, POST, PUT, DELETE) + * @param collectionName The name for the collection + * @returns A HoppCollection object with example pet store requests + */ +export function createExamplePetStoreCollection( + collectionName: string = "Pet Store Mock Server" +): HoppCollection { + const requests: HoppRESTRequest[] = [ + // GET - List all pets + makeRESTRequest({ + id: uniqueID(), + name: "Get All Pets", + method: "GET", + endpoint: `${MOCK_URL_VAR}/pets`, + params: [], + headers: [], + auth: { + authType: "inherit", + authActive: true, + }, + preRequestScript: "", + testScript: "", + body: { + contentType: null, + body: null, + }, + requestVariables: [], + responses: { + [uniqueID()]: { + status: 200, + body: JSON.stringify( + [ + { + id: 1, + name: "Buddy", + species: "Dog", + breed: "Golden Retriever", + age: 3, + status: "available", + }, + { + id: 2, + name: "Whiskers", + species: "Cat", + breed: "Siamese", + age: 2, + status: "available", + }, + { + id: 3, + name: "Charlie", + species: "Dog", + breed: "Beagle", + age: 4, + status: "adopted", + }, + ], + null, + 2 + ), + headers: [ + { + key: "Content-Type", + value: "application/json", + active: true, + description: "", + }, + ], + }, + }, + }), + + // GET - Get a single pet by ID + makeRESTRequest({ + id: uniqueID(), + name: "Get Pet by ID", + method: "GET", + endpoint: `${MOCK_URL_VAR}/pets/1`, + params: [], + headers: [], + auth: { + authType: "inherit", + authActive: true, + }, + preRequestScript: "", + testScript: "", + body: { + contentType: null, + body: null, + }, + requestVariables: [], + responses: { + [uniqueID()]: { + status: 200, + body: JSON.stringify( + { + id: 1, + name: "Buddy", + species: "Dog", + breed: "Golden Retriever", + age: 3, + status: "available", + description: "Friendly and energetic golden retriever", + vaccinated: true, + neutered: true, + }, + null, + 2 + ), + headers: [ + { + key: "Content-Type", + value: "application/json", + active: true, + description: "", + }, + ], + }, + }, + }), + + // POST - Create a new pet + makeRESTRequest({ + id: uniqueID(), + name: "Create New Pet", + method: "POST", + endpoint: `${MOCK_URL_VAR}/pets`, + params: [], + headers: [ + { + key: "Content-Type", + value: "application/json", + active: true, + description: "", + }, + ], + auth: { + authType: "inherit", + authActive: true, + }, + preRequestScript: "", + testScript: "", + body: { + contentType: "application/json", + body: JSON.stringify( + { + name: "Max", + species: "Dog", + breed: "Labrador", + age: 2, + status: "available", + description: "Playful labrador looking for a home", + vaccinated: true, + neutered: false, + }, + null, + 2 + ), + }, + requestVariables: [], + responses: { + [uniqueID()]: { + status: 201, + body: JSON.stringify( + { + id: 4, + name: "Max", + species: "Dog", + breed: "Labrador", + age: 2, + status: "available", + description: "Playful labrador looking for a home", + vaccinated: true, + neutered: false, + createdAt: new Date().toISOString(), + }, + null, + 2 + ), + headers: [ + { + key: "Content-Type", + value: "application/json", + active: true, + description: "", + }, + { + key: "Location", + value: "/pets/4", + active: true, + description: "", + }, + ], + }, + }, + }), + + // PUT - Update an existing pet + makeRESTRequest({ + id: uniqueID(), + name: "Update Pet", + method: "PUT", + endpoint: `${MOCK_URL_VAR}/pets/1`, + params: [], + headers: [ + { + key: "Content-Type", + value: "application/json", + active: true, + description: "", + }, + ], + auth: { + authType: "inherit", + authActive: true, + }, + preRequestScript: "", + testScript: "", + body: { + contentType: "application/json", + body: JSON.stringify( + { + name: "Buddy", + species: "Dog", + breed: "Golden Retriever", + age: 4, + status: "adopted", + description: "Friendly golden retriever - Now adopted!", + vaccinated: true, + neutered: true, + }, + null, + 2 + ), + }, + requestVariables: [], + responses: { + [uniqueID()]: { + status: 200, + body: JSON.stringify( + { + id: 1, + name: "Buddy", + species: "Dog", + breed: "Golden Retriever", + age: 4, + status: "adopted", + description: "Friendly golden retriever - Now adopted!", + vaccinated: true, + neutered: true, + updatedAt: new Date().toISOString(), + }, + null, + 2 + ), + headers: [ + { + key: "Content-Type", + value: "application/json", + active: true, + description: "", + }, + ], + }, + }, + }), + + // DELETE - Delete a pet + makeRESTRequest({ + id: uniqueID(), + name: "Delete Pet", + method: "DELETE", + endpoint: `${MOCK_URL_VAR}/pets/3`, + params: [], + headers: [], + auth: { + authType: "inherit", + authActive: true, + }, + preRequestScript: "", + testScript: "", + body: { + contentType: null, + body: null, + }, + requestVariables: [], + responses: { + [uniqueID()]: { + status: 204, + body: "", + headers: [], + }, + }, + }), + ] + + return makeCollection({ + name: collectionName, + folders: [], + requests, + auth: { + authType: "inherit", + authActive: true, + }, + headers: [], + }) +} diff --git a/packages/hoppscotch-common/src/helpers/mockServer/exampleMockCollection.ts b/packages/hoppscotch-common/src/helpers/mockServer/exampleMockCollection.ts new file mode 100644 index 00000000..d1a98586 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/mockServer/exampleMockCollection.ts @@ -0,0 +1,343 @@ +import { pipe } from "fp-ts/function" +import * as TE from "fp-ts/TaskEither" +import * as E from "fp-ts/Either" +import { + HoppRESTRequest, + RESTReqSchemaVersion, + makeCollection, +} from "@hoppscotch/data" +import { createNewRootCollection } from "~/helpers/backend/mutations/TeamCollection" +import { createRequestInCollection } from "~/helpers/backend/mutations/TeamRequest" +import { runMutation } from "~/helpers/backend/GQLClient" +import { + CreateRestRootUserCollectionDocument, + CreateRestRootUserCollectionMutation, + CreateRestRootUserCollectionMutationVariables, + CreateRestUserRequestDocument, + CreateRestUserRequestMutation, + CreateRestUserRequestMutationVariables, +} from "~/helpers/backend/graphql" +import { addRESTCollection } from "~/newstore/collections" + +/** + * Get example REST requests for mock server collection + */ +export function getExampleMockRequests(): HoppRESTRequest[] { + const petBody = JSON.stringify( + { + id: 1, + category: { + id: 1, + name: "string", + }, + name: "doggie", + photoUrls: ["string"], + tags: [], + status: "available", + }, + null, + 2 + ) + + const oauthAuth = { + authType: "oauth-2" as const, + authActive: true, + grantTypeInfo: { + authEndpoint: "<>/oauth/authorize", + clientID: "", + grantType: "IMPLICIT" as const, + scopes: "write:pets read:pets", + token: "", + authRequestParams: [], + refreshRequestParams: [], + }, + addTo: "HEADERS" as const, + } + + const requests: HoppRESTRequest[] = [ + // addPet request + { + v: RESTReqSchemaVersion, + name: "addPet", + method: "POST", + endpoint: "<>/v2/pet", + params: [], + headers: [], + preRequestScript: "", + testScript: "", + body: { + contentType: "application/json", + body: petBody, + }, + auth: oauthAuth, + requestVariables: [], + responses: {}, + }, + // updatePet request + { + v: RESTReqSchemaVersion, + name: "updatePet", + method: "PUT", + endpoint: "<>/v2/pet", + params: [], + headers: [], + preRequestScript: "", + testScript: "", + body: { + contentType: "application/json", + body: petBody, + }, + auth: oauthAuth, + requestVariables: [], + responses: {}, + }, + // findByStatus request + { + v: RESTReqSchemaVersion, + name: "findByStatus", + method: "GET", + endpoint: "<>/v2/pet/findByStatus", + params: [ + { + key: "status", + value: "available", + active: true, + description: "", + }, + ], + headers: [], + preRequestScript: "", + testScript: "", + body: { + contentType: null, + body: null, + }, + auth: oauthAuth, + requestVariables: [], + responses: {}, + }, + // getPetById request + { + v: RESTReqSchemaVersion, + name: "getPetById", + method: "GET", + endpoint: "<>/v2/pet/1", + params: [], + headers: [], + preRequestScript: "", + testScript: "", + body: { + contentType: null, + body: null, + }, + auth: { + authType: "api-key", + authActive: true, + key: "api_key", + value: "", + addTo: "HEADERS", + }, + requestVariables: [], + responses: {}, + }, + // updatePetWithForm request + { + v: RESTReqSchemaVersion, + name: "updatePetWithForm", + method: "POST", + endpoint: "<>/v2/pet/1", + params: [], + headers: [], + preRequestScript: "", + testScript: "", + body: { + contentType: "application/x-www-form-urlencoded", + body: "name=doggie&status=available", + }, + auth: oauthAuth, + requestVariables: [], + responses: {}, + }, + // deletePet request + { + v: RESTReqSchemaVersion, + name: "deletePet", + method: "DELETE", + endpoint: "<>/v2/pet/1", + params: [], + headers: [ + { + key: "api_key", + value: "", + active: true, + description: "", + }, + ], + preRequestScript: "", + testScript: "", + body: { + contentType: null, + body: null, + }, + auth: oauthAuth, + requestVariables: [], + responses: {}, + }, + ] + + return requests +} + +/** + * Create a mock collection for team workspace + */ +export async function createMockCollectionForTeam( + teamID: string, + collectionName: string +): Promise> { + // Create the root collection + const collectionResult = await pipe( + createNewRootCollection(collectionName, teamID), + TE.match( + (error) => E.left(`Failed to create collection: ${error}`), + (collection) => E.right(collection) + ) + )() + + if (E.isLeft(collectionResult)) { + return collectionResult + } + + const collectionID = collectionResult.right.createRootCollection.id + + // Create requests in the collection + const requests = getExampleMockRequests() + + for (const request of requests) { + const requestResult = await pipe( + createRequestInCollection(collectionID, { + request: JSON.stringify(request), + teamID, + title: request.name, + }), + TE.match( + (error) => E.left(`Failed to create request: ${error}`), + (req) => E.right(req) + ) + )() + + if (E.isLeft(requestResult)) { + // Log error but continue with other requests + console.error( + "Failed to create request:", + request.name, + requestResult.left + ) + } + } + + return E.right({ + id: collectionID, + name: collectionName, + }) +} + +/** + * Create a mock collection for personal workspace + * Uses backend GraphQL mutations to create the collection with proper backend ID + */ +export async function createMockCollectionForPersonal( + collectionName: string +): Promise> { + // Prepare collection data + const data = { + auth: { + authType: "inherit" as const, + authActive: true, + }, + headers: [], + variables: [], + } + + // Create the root collection using GraphQL mutation + const collectionResult = await pipe( + runMutation< + CreateRestRootUserCollectionMutation, + CreateRestRootUserCollectionMutationVariables, + "" + >(CreateRestRootUserCollectionDocument, { + title: collectionName, + data: JSON.stringify(data), + }), + TE.match( + (error) => E.left(`Failed to create collection: ${error}`), + (response) => E.right(response) + ) + )() + + if (E.isLeft(collectionResult)) { + return collectionResult + } + + // Extract the collection ID from the response + const collectionID = collectionResult.right.createRESTRootUserCollection.id + + // Create requests in the collection using GraphQL mutation + const requests = getExampleMockRequests() + const createdRequests: HoppRESTRequest[] = [] + + for (const request of requests) { + const requestResult = await pipe( + runMutation< + CreateRestUserRequestMutation, + CreateRestUserRequestMutationVariables, + "" + >(CreateRestUserRequestDocument, { + collectionID, + title: request.name, + request: JSON.stringify(request), + }), + TE.match( + (error) => E.left(`Failed to create request: ${error}`), + (req) => E.right(req) + ) + )() + + if (E.isLeft(requestResult)) { + // Log error but continue with other requests + console.error( + "Failed to create request:", + request.name, + requestResult.left + ) + } else { + // Add the request ID to the created request + const createdRequest = { + ...request, + id: requestResult.right.createRESTUserRequest.id, + } + createdRequests.push(createdRequest) + } + } + + // Create a HoppCollection object and add it to the store immediately + const collection = makeCollection({ + name: collectionName, + folders: [], + requests: createdRequests, + auth: data.auth, + headers: data.headers, + variables: data.variables, + }) + + // Add the backend ID to the collection + collection.id = collectionID + + // Add the collection to the store so it's visible immediately + addRESTCollection(collection) + + return E.right({ + id: collectionID, + name: collectionName, + }) +}