api-client/packages/hoppscotch-common/src/services/persistence/__tests__/index.spec.ts
Mir Arif Hasan 904a1b0405
chore: security patch for the dependency chain v2025.11.0 (#5590)
Bump dependencies and account for breaking changes.

---------

Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
2025-11-24 14:21:29 +05:30

1934 lines
65 KiB
TypeScript

/* eslint-disable no-restricted-globals, no-restricted-syntax */
import * as E from "fp-ts/Either"
import {
translateToNewGQLCollection,
translateToNewRESTCollection,
} from "@hoppscotch/data"
import { watchDebounced } from "@vueuse/core"
import { TestContainer } from "dioc/testing"
import { cloneDeep } from "lodash-es"
import superjson from "superjson"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { getKernelMode, initKernel } from "@hoppscotch/kernel"
import { Store } from "~/kernel"
import { MQTTRequest$, setMQTTRequest } from "~/newstore/MQTTSession"
import { SSERequest$, setSSERequest } from "~/newstore/SSESession"
import { SIORequest$, setSIORequest } from "~/newstore/SocketIOSession"
import { WSRequest$, setWSRequest } from "~/newstore/WebSocketSession"
import {
graphqlCollectionStore,
restCollectionStore,
setGraphqlCollections,
setRESTCollections,
} from "~/newstore/collections"
import {
addGlobalEnvVariable,
environments$,
globalEnv$,
replaceEnvironments,
selectedEnvironmentIndex$,
setGlobalEnvVariables,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import {
graphqlHistoryStore,
restHistoryStore,
setGraphqlHistoryEntries,
setRESTHistoryEntries,
translateToNewGQLHistory,
translateToNewRESTHistory,
} from "~/newstore/history"
import { bulkApplyLocalState, localStateStore } from "~/newstore/localstate"
import {
applySetting,
bulkApplySettings,
performSettingsDataMigrations,
settingsStore,
} from "~/newstore/settings"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { GQLTabService } from "~/services/tab/graphql"
import { RESTTabService } from "~/services/tab/rest"
import {
PersistenceService,
STORE_KEYS,
STORE_NAMESPACE,
} from "../../persistence"
import {
ENVIRONMENTS_MOCK,
GLOBAL_ENV_MOCK,
GQL_COLLECTIONS_MOCK,
GQL_HISTORY_MOCK,
GQL_TAB_STATE_MOCK,
MQTT_REQUEST_MOCK,
REST_COLLECTIONS_MOCK,
REST_HISTORY_MOCK,
REST_TAB_STATE_MOCK,
SECRET_ENVIRONMENTS_MOCK,
SELECTED_ENV_INDEX_MOCK,
SOCKET_IO_REQUEST_MOCK,
SSE_REQUEST_MOCK,
VUEX_DATA_MOCK,
WEBSOCKET_REQUEST_MOCK,
} from "./__mocks__"
initKernel(getKernelMode())
vi.mock("~/modules/i18n", () => {
return {
__esModule: true,
getI18n: vi.fn().mockImplementation(() => () => "test"),
}
})
// Define modules that are shared across methods here
vi.mock("@vueuse/core", async (importOriginal) => {
const actualModule: Record<string, unknown> = await importOriginal()
return {
...actualModule,
watchDebounced: vi.fn(),
}
})
vi.mock("~/newstore/environments", () => {
return {
addGlobalEnvVariable: vi.fn(),
setGlobalEnvVariables: vi.fn(),
replaceEnvironments: vi.fn(),
setSelectedEnvironmentIndex: vi.fn(),
environments$: {
subscribe: vi.fn(),
},
globalEnv$: {
subscribe: vi.fn(),
},
selectedEnvironmentIndex$: {
subscribe: vi.fn(),
},
}
})
vi.mock("~/newstore/settings", () => {
return {
applySetting: vi.fn(),
}
})
const toastErrorFn = vi.fn()
vi.mock("~/composables/toast", () => {
return {
useToast: () => ({ error: toastErrorFn }),
}
})
/**
* Helper functions
*/
const spyOnGetItem = () => vi.spyOn(Storage.prototype, "getItem")
const spyOnRemoveItem = () => vi.spyOn(Storage.prototype, "removeItem")
const spyOnSetItem = () => vi.spyOn(Storage.prototype, "setItem")
const schemaVersionKey = `${STORE_NAMESPACE}:${STORE_KEYS.SCHEMA_VERSION}`
const getStoreItem = async (key: string) => {
const storedData = window.localStorage.getItem(key)
if (!storedData) return
const parsedData = superjson.parse<{ data: string }>(storedData)
return JSON.stringify(parsedData.data)
}
const setStoreItem = async <T>(key: string, value: T) => {
const storedData = {
schemaVersion: 1,
metadata: {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
namespace: STORE_NAMESPACE,
encrypted: false,
compressed: false,
},
data: value,
}
window.localStorage.setItem(key, superjson.stringify(storedData))
}
const bindPersistenceService = ({
mockGQLTabService = false,
mockRESTTabService = false,
mockSecretEnvironmentsService = false,
mock = {},
}: {
mockGQLTabService?: boolean
mockRESTTabService?: boolean
mockSecretEnvironmentsService?: boolean
mock?: Record<string, unknown>
} = {}) => {
const container = new TestContainer()
if (mockGQLTabService) {
container.bindMock(GQLTabService, mock)
}
if (mockRESTTabService) {
container.bindMock(RESTTabService, mock)
}
if (mockSecretEnvironmentsService) {
container.bindMock(SecretEnvironmentService, mock)
}
container.bind(PersistenceService)
const service = container.bind(PersistenceService)
return service
}
const invokeSetupLocalPersistence = async (
serviceBindMock?: Record<string, unknown>
) => {
const service = bindPersistenceService(serviceBindMock)
await service.setupFirst()
await service.setupLater()
}
describe("PersistenceService", () => {
beforeEach(() => {
window.localStorage.clear()
})
beforeEach(async () => {
await Store.remove(STORE_NAMESPACE, STORE_KEYS.SCHEMA_VERSION)
})
afterEach(() => {
// Clear all mocks
vi.clearAllMocks()
// Restore the original implementation for any spied functions
vi.restoreAllMocks()
})
describe("setup", () => {
describe("Check and migrate old settings", () => {
// Set of keys read from localStorage across test cases
const bgColorKey = "BG_COLOR"
const nuxtColorModeKey = "nuxt-color-mode"
const selectedEnvIndexKey = "selectedEnvIndex"
const themeColorKey = "THEME_COLOR"
const vuexKey = "vuex"
const storagePrefix = "persistence.v1:"
beforeEach(() => {
window.localStorage.clear()
})
beforeEach(async () => {
await Store.remove(STORE_NAMESPACE, STORE_KEYS.SCHEMA_VERSION)
})
it(`sets the selected environment index type as "NO_ENV" in localStorage if the value retrieved for ${selectedEnvIndexKey} is "-1"`, async () => {
window.localStorage.setItem(selectedEnvIndexKey, "-1")
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(selectedEnvIndexKey)
expect(setItemSpy).toHaveBeenCalledWith(
`${storagePrefix}selectedEnv`,
expect.stringContaining(
JSON.stringify({
type: "NO_ENV_SELECTED",
})
)
)
})
it(`sets the selected environment index type as "MY_ENV" in localStorage if the value retrieved for "${selectedEnvIndexKey}" is greater than "0"`, async () => {
window.localStorage.setItem(selectedEnvIndexKey, "1")
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(selectedEnvIndexKey)
expect(setItemSpy).toHaveBeenCalledWith(
`${storagePrefix}selectedEnv`,
expect.stringContaining(
JSON.stringify({
type: "MY_ENV",
index: 1,
})
)
)
})
it(`skips schema parsing and setting other properties if ${vuexKey} read from localStorage is an empty entity`, async () => {
window.localStorage.setItem(vuexKey, JSON.stringify({}))
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(vuexKey)
expect(toastErrorFn).not.toHaveBeenCalled()
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
`${storagePrefix}schema_version`,
expect.stringMatching(/"schemaVersion":1/)
)
})
it(`shows an error and sets the entry as a backup in localStorage if "${vuexKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `vuex`
// `postwoman.settings.CURRENT_INTERCEPTOR_ID` -> `string`
const vuexData = {
...VUEX_DATA_MOCK,
postwoman: {
...VUEX_DATA_MOCK.postwoman,
settings: {
...VUEX_DATA_MOCK.postwoman.settings,
CURRENT_INTERCEPTOR_ID: 1234,
},
},
}
window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(vuexKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(vuexKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${vuexKey}-backup`,
expect.stringContaining(JSON.stringify(vuexData))
)
})
it(`shows an error and sets the entry as a backup in localStorage if "${themeColorKey}" read from localStorage doesn't match the schema`, async () => {
const vuexData = cloneDeep(VUEX_DATA_MOCK)
window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
const themeColorValue = "invalid-color"
window.localStorage.setItem(themeColorKey, themeColorValue)
const getItemSpy = spyOnGetItem()
const removeItemSpy = spyOnRemoveItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(vuexKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(themeColorKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${themeColorKey}-backup`,
themeColorValue
)
expect(applySetting).not.toHaveBeenCalledWith(
themeColorKey,
themeColorValue
)
expect(removeItemSpy).toHaveBeenCalledWith(themeColorKey)
})
it(`shows an error and sets the entry as a backup in localStorage if "${nuxtColorModeKey}" read from localStorage doesn't match the schema`, async () => {
const vuexData = cloneDeep(VUEX_DATA_MOCK)
window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
const nuxtColorModeValue = "invalid-color"
window.localStorage.setItem(nuxtColorModeKey, nuxtColorModeValue)
const getItemSpy = spyOnGetItem()
const removeItemSpy = spyOnRemoveItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(vuexKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(nuxtColorModeKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${nuxtColorModeKey}-backup`,
nuxtColorModeValue
)
expect(applySetting).not.toHaveBeenCalledWith(
bgColorKey,
nuxtColorModeValue
)
expect(removeItemSpy).toHaveBeenCalledWith(nuxtColorModeKey)
})
it(`extracts individual properties from the key "${vuexKey}" and sets them in localStorage`, async () => {
const vuexData = cloneDeep(VUEX_DATA_MOCK)
window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
const themeColor = "red"
const nuxtColorMode = "dark"
window.localStorage.setItem(themeColorKey, themeColor)
window.localStorage.setItem(nuxtColorModeKey, nuxtColorMode)
const getItemSpy = spyOnGetItem()
const removeItemSpy = spyOnRemoveItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(vuexKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(nuxtColorModeKey)
expect(setItemSpy).not.toHaveBeenCalledWith(
`${nuxtColorModeKey}-backup`
)
// Check settings is saved with new format
expect(setItemSpy).toHaveBeenCalledWith(
`${storagePrefix}settings`,
expect.stringContaining(JSON.stringify(vuexData.postwoman.settings))
)
const { postwoman } = vuexData
delete postwoman.settings
// Check collections is saved with new format
expect(setItemSpy).toHaveBeenCalledWith(
`${storagePrefix}restCollections`,
expect.stringContaining(JSON.stringify(postwoman.collections))
)
delete postwoman.collections
// Check graphql collections is saved with new format
expect(setItemSpy).toHaveBeenCalledWith(
`${storagePrefix}gqlCollections`,
expect.stringContaining(JSON.stringify(postwoman.collectionsGraphql))
)
delete postwoman.collectionsGraphql
// Check environments is saved with new format
expect(setItemSpy).toHaveBeenCalledWith(
`${storagePrefix}environments`,
expect.stringContaining(JSON.stringify(postwoman.environments))
)
delete postwoman.environments
// Check theme color handling
expect(getItemSpy).toHaveBeenCalledWith(themeColorKey)
expect(applySetting).toHaveBeenCalledWith(themeColorKey, themeColor)
expect(removeItemSpy).toHaveBeenCalledWith(themeColorKey)
expect(window.localStorage.getItem(themeColorKey)).toBe(null)
// Check color mode handling
expect(getItemSpy).toHaveBeenCalledWith(nuxtColorModeKey)
expect(applySetting).toHaveBeenCalledWith(bgColorKey, nuxtColorMode)
expect(removeItemSpy).toHaveBeenCalledWith(nuxtColorModeKey)
expect(window.localStorage.getItem(nuxtColorModeKey)).toBe(null)
})
})
describe("Setup local state persistence", () => {
// Key read from localStorage across test cases
const localStateKey = `${STORE_NAMESPACE}:${STORE_KEYS.LOCAL_STATE}`
it(`shows an error and sets the entry as a backup in localStorage if "${localStateKey}" read from localStorage has a value which is not a "string" or "undefined"`, async () => {
const localStateData = {
REMEMBERED_TEAM_ID: null,
}
await setStoreItem(localStateKey, localStateData)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(localStateKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(localStateKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${localStateKey}-backup`,
expect.stringContaining(JSON.stringify(localStateData))
)
})
it(`shows an error and sets the entry as a backup in localStorage if "${localStateKey}" read from localStorage has an invalid key`, async () => {
const localStateData = {
INVALID_KEY: null,
}
await setStoreItem(localStateKey, localStateData)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(localStateKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(localStateKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${localStateKey}-backup`,
expect.stringContaining(JSON.stringify(localStateData))
)
})
it(`schema parsing succeeds if there is no "${localStateKey}" key present in localStorage where the fallback of "{}" is chosen`, async () => {
window.localStorage.removeItem(localStateKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(localStateKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(localStateKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringMatching(/"schemaVersion":1/)
)
})
it(`reads the value for "${localStateKey}" key from localStorage, invokes "bulkApplyLocalState" function if a value is yielded and subscribes to "localStateStore" updates`, async () => {
vi.mock("~/newstore/localstate", () => {
return {
bulkApplyLocalState: vi.fn(),
localStateStore: {
subject$: {
subscribe: vi.fn(),
},
},
}
})
const localStateData = {
REMEMBERED_TEAM_ID: "test-id",
}
await setStoreItem(localStateKey, localStateData)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(localStateKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(localStateKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringMatching(/"schemaVersion":1/)
)
expect(bulkApplyLocalState).toHaveBeenCalledWith(localStateData)
expect(localStateStore.subject$.subscribe).toHaveBeenCalledWith(
expect.any(Function)
)
})
})
describe("setup settings persistence", () => {
// Key read from localStorage across test cases
const settingsKey = `${STORE_NAMESPACE}:${STORE_KEYS.SETTINGS}`
it(`shows an error and sets the entry as a backup in localStorage if "${settingsKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `settings`
// Expected values are booleans
const settings = {
EXTENSIONS_ENABLED: "true",
PROXY_ENABLED: "true",
}
await setStoreItem(settingsKey, settings)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(settingsKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(settingsKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${settingsKey}-backup`,
expect.stringContaining(JSON.stringify(settings))
)
})
it(`schema parsing succeeds if there is no "${settingsKey}" key present in localStorage where the fallback of "{}" is chosen`, async () => {
window.localStorage.removeItem(settingsKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(settingsKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(settingsKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringMatching(/"schemaVersion":1/)
)
})
it(`reads the value for "${settingsKey}" from localStorage, invokes "performSettingsDataMigrations" and "bulkApplySettings" functions as required and subscribes to "settingsStore" updates`, async () => {
vi.mock("~/newstore/settings", async (importOriginal) => {
const actualModule: Record<string, unknown> = await importOriginal()
return {
...actualModule,
applySetting: vi.fn(),
bulkApplySettings: vi.fn(),
performSettingsDataMigrations: vi
.fn()
.mockImplementation((data: any) => data),
settingsStore: {
subject$: {
subscribe: vi.fn(),
},
},
}
})
const { settings } = VUEX_DATA_MOCK.postwoman
await setStoreItem(settingsKey, settings)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(settingsKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(settingsKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringMatching(/"schemaVersion":1/)
)
expect(performSettingsDataMigrations).toHaveBeenCalledWith(settings)
expect(bulkApplySettings).toHaveBeenCalledWith(settings)
expect(settingsStore.subject$.subscribe).toHaveBeenCalledWith(
expect.any(Function)
)
})
})
describe("setup history persistence", () => {
// Keys read from localStorage across test cases
const historyKey = `${STORE_NAMESPACE}:${STORE_KEYS.REST_HISTORY}`
const graphqlHistoryKey = `${STORE_NAMESPACE}:${STORE_KEYS.GQL_HISTORY}`
it(`shows an error and sets the entry as a backup in localStorage if "${historyKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `history`
// `v` -> `number`
const restHistoryData = [{ ...REST_HISTORY_MOCK, v: "1" }]
await setStoreItem(historyKey, restHistoryData)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(historyKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(historyKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${historyKey}-backup`,
expect.stringContaining(JSON.stringify(restHistoryData))
)
})
it(`REST history schema parsing succeeds if there is no "${historyKey}" key present in localStorage where the fallback of "[]" is chosen`, async () => {
window.localStorage.removeItem(historyKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(historyKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(historyKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringMatching(/"schemaVersion":1/)
)
})
it(`shows an error and sets the entry as a backup in localStorage if "${graphqlHistoryKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `graphqlHistory`
// `v` -> `number`
const graphqlHistoryData = [{ ...GQL_HISTORY_MOCK, v: "1" }]
await setStoreItem(graphqlHistoryKey, graphqlHistoryData)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(graphqlHistoryKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(graphqlHistoryKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${graphqlHistoryKey}-backup`,
expect.stringContaining(JSON.stringify(graphqlHistoryData))
)
})
it(`GQL history schema parsing succeeds if there is no "${graphqlHistoryKey}" key present in localStorage where the fallback of "[]" is chosen`, async () => {
window.localStorage.removeItem(graphqlHistoryKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(graphqlHistoryKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(graphqlHistoryKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringMatching(/"schemaVersion":1/)
)
})
it("reads REST and GQL history entries from localStorage, translates them to the new format, writes back the updates and subscribes to the respective store for updates", async () => {
vi.mock("~/newstore/history", () => {
return {
setGraphqlHistoryEntries: vi.fn(),
setRESTHistoryEntries: vi.fn(),
translateToNewGQLHistory: vi
.fn()
.mockImplementation((data: any) => data),
translateToNewRESTHistory: vi
.fn()
.mockImplementation((data: any) => data),
graphqlHistoryStore: {
subject$: {
subscribe: vi.fn(),
},
},
restHistoryStore: {
subject$: {
subscribe: vi.fn(),
},
},
}
})
const restHistory = REST_HISTORY_MOCK
const gqlHistory = GQL_HISTORY_MOCK
await setStoreItem(historyKey, restHistory)
await setStoreItem(graphqlHistoryKey, gqlHistory)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(historyKey)
expect(getItemSpy).toHaveBeenCalledWith(graphqlHistoryKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(
historyKey,
graphqlHistoryKey
)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringMatching(/"schemaVersion":1/)
)
expect(translateToNewRESTHistory).toHaveBeenCalled()
expect(translateToNewGQLHistory).toHaveBeenCalled()
expect(setRESTHistoryEntries).toHaveBeenCalledWith(restHistory)
expect(setGraphqlHistoryEntries).toHaveBeenCalledWith(gqlHistory)
expect(restHistoryStore.subject$.subscribe).toHaveBeenCalledWith(
expect.any(Function)
)
expect(graphqlHistoryStore.subject$.subscribe).toHaveBeenCalledWith(
expect.any(Function)
)
})
})
describe("setup collections persistence", () => {
// Keys read from localStorage across test cases
const collectionsRESTKey = `${STORE_NAMESPACE}:${STORE_KEYS.REST_COLLECTIONS}`
const collectionsGraphqlKey = `${STORE_NAMESPACE}:${STORE_KEYS.GQL_COLLECTIONS}`
it(`shows an error and sets the entry as a backup in localStorage if "${collectionsRESTKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `collections`
// `v` -> `number`
const restCollectionsData = [{ ...REST_COLLECTIONS_MOCK, v: "1" }]
await setStoreItem(collectionsRESTKey, restCollectionsData)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(collectionsRESTKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(collectionsRESTKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${collectionsRESTKey}-backup`,
expect.stringContaining(JSON.stringify(restCollectionsData))
)
})
it(`REST collections schema parsing succeeds if there is no "${collectionsRESTKey}" key present in localStorage where the fallback of "[]" is chosen`, async () => {
window.localStorage.removeItem(collectionsRESTKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(collectionsRESTKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(collectionsRESTKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
})
it(`shows an error and sets the entry as a backup in localStorage if "${collectionsGraphqlKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `collectionsGraphql`
// `v` -> `number`
const graphqlCollectionsData = [{ ...GQL_COLLECTIONS_MOCK, v: "1" }]
await setStoreItem(collectionsGraphqlKey, graphqlCollectionsData)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(collectionsGraphqlKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(collectionsGraphqlKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${collectionsGraphqlKey}-backup`,
expect.stringContaining(JSON.stringify(graphqlCollectionsData))
)
})
it(`GQL collections schema parsing succeeds if there is no "${collectionsGraphqlKey}" key present in localStorage where the fallback of "[]" is chosen`, async () => {
window.localStorage.removeItem(collectionsGraphqlKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(collectionsGraphqlKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(collectionsGraphqlKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
})
it("reads REST and GQL collection entries from localStorage, translates them to the new format, writes back the updates and subscribes to the respective store for updates", async () => {
vi.mock("@hoppscotch/data", async (importOriginal) => {
const actualModule: Record<string, unknown> = await importOriginal()
return {
...actualModule,
translateToNewGQLCollection: vi
.fn()
.mockImplementation((data: any) => data),
translateToNewRESTCollection: vi
.fn()
.mockImplementation((data: any) => data),
}
})
vi.mock("~/newstore/collections", () => {
return {
setGraphqlCollections: vi.fn(),
setRESTCollections: vi.fn(),
graphqlCollectionStore: {
subject$: {
subscribe: vi.fn(),
},
},
restCollectionStore: {
subject$: {
subscribe: vi.fn(),
},
},
}
})
const restCollections = REST_COLLECTIONS_MOCK
const gqlCollections = GQL_COLLECTIONS_MOCK
await setStoreItem(collectionsRESTKey, restCollections)
await setStoreItem(collectionsGraphqlKey, gqlCollections)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(collectionsRESTKey)
expect(getItemSpy).toHaveBeenCalledWith(collectionsGraphqlKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(
collectionsRESTKey,
collectionsGraphqlKey
)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
expect(translateToNewGQLCollection).toHaveBeenCalled()
expect(translateToNewRESTCollection).toHaveBeenCalled()
expect(setRESTCollections).toHaveBeenCalledWith(restCollections)
expect(setGraphqlCollections).toHaveBeenCalledWith(gqlCollections)
expect(graphqlCollectionStore.subject$.subscribe).toHaveBeenCalledWith(
expect.any(Function)
)
expect(restCollectionStore.subject$.subscribe).toHaveBeenCalledWith(
expect.any(Function)
)
})
})
describe("setup environments persistence", () => {
// Key read from localStorage across test cases
const environmentsKey = `${STORE_NAMESPACE}:${STORE_KEYS.ENVIRONMENTS}`
it(`shows an error and sets the entry as a backup in localStorage if "${environmentsKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `environments`
const environments = [
// `entries` -> `variables`
// no name for the environment
{
v: 1,
id: "ENV_1",
entries: [{ key: "test-key", value: "test-value", secret: false }],
},
]
await setStoreItem(environmentsKey, environments)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(environmentsKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(environmentsKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${environmentsKey}-backup`,
expect.stringContaining(JSON.stringify(environments))
)
})
it(`separates "globals" entries from "${environmentsKey}", subscribes to the "environmentStore" and updates localStorage entries`, async () => {
const environments = cloneDeep(ENVIRONMENTS_MOCK)
await setStoreItem(environmentsKey, environments)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(environmentsKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(environmentsKey)
expect(setItemSpy).not.toHaveBeenCalledWith(`${environmentsKey}-backup`)
expect(addGlobalEnvVariable).toHaveBeenCalledWith(
environments[0].variables[0]
)
// Removes `globals` from environments
environments.splice(0, 1)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringMatching(/"schemaVersion":1/)
)
expect(replaceEnvironments).toBeCalledWith(environments)
expect(environments$.subscribe).toHaveBeenCalledWith(
expect.any(Function)
)
})
})
describe("setup selected environment persistence", () => {
// Key read from localStorage across test cases
const selectedEnvIndexKey = `${STORE_NAMESPACE}:${STORE_KEYS.SELECTED_ENV}`
it(`shows an error and sets the entry as a backup in localStorage if "${selectedEnvIndexKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `selectedEnvIndex`
// `index` -> `number`
const selectedEnvIndex = { ...SELECTED_ENV_INDEX_MOCK, index: "1" }
await setStoreItem(selectedEnvIndexKey, selectedEnvIndex)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(selectedEnvIndexKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(selectedEnvIndexKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${selectedEnvIndexKey}-backup`,
expect.stringContaining(JSON.stringify(selectedEnvIndex))
)
})
it(`schema parsing succeeds if there is no "${selectedEnvIndexKey}" key present in localStorage where the fallback of "null" is chosen`, async () => {
window.localStorage.removeItem(selectedEnvIndexKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(selectedEnvIndexKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(selectedEnvIndexKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
})
it(`sets it to the store if there is a value associated with the "${selectedEnvIndexKey}" key in localStorage`, async () => {
const selectedEnvIndex = SELECTED_ENV_INDEX_MOCK
await setStoreItem(selectedEnvIndexKey, selectedEnvIndex)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(selectedEnvIndexKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(selectedEnvIndexKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
expect(setSelectedEnvironmentIndex).toHaveBeenCalledWith(
selectedEnvIndex
)
expect(selectedEnvironmentIndex$.subscribe).toHaveBeenCalledWith(
expect.any(Function)
)
})
it(`sets it to "NO_ENV_SELECTED" if there is no value associated with the "${selectedEnvIndexKey}" in localStorage`, async () => {
window.localStorage.removeItem(selectedEnvIndexKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(selectedEnvIndexKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(selectedEnvIndexKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
expect(setSelectedEnvironmentIndex).toHaveBeenCalledWith({
type: "NO_ENV_SELECTED",
})
expect(selectedEnvironmentIndex$.subscribe).toHaveBeenCalledWith(
expect.any(Function)
)
})
})
describe("setup secret Environments persistence", () => {
// Key read from localStorage across test cases
const secretEnvironmentsKey = `${STORE_NAMESPACE}:${STORE_KEYS.SECRET_ENVIRONMENTS}`
const loadSecretEnvironmentsFromPersistedStateFn = vi.fn()
const mock = {
loadSecretEnvironmentsFromPersistedState:
loadSecretEnvironmentsFromPersistedStateFn,
}
it(`shows an error and sets the entry as a backup in localStorage if "${secretEnvironmentsKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `secretEnvironments`
const secretEnvironments = {
clryz7ir7002al4162bsj0azg: {
key: "ENV_KEY",
value: "ENV_VALUE",
},
}
await setStoreItem(secretEnvironmentsKey, secretEnvironments)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(secretEnvironmentsKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(secretEnvironmentsKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${secretEnvironmentsKey}-backup`,
expect.stringContaining(JSON.stringify(secretEnvironments))
)
})
it("loads secret environments from the state persisted in localStorage and sets watcher for `persistableSecretEnvironment`", async () => {
const secretEnvironment = SECRET_ENVIRONMENTS_MOCK
await setStoreItem(secretEnvironmentsKey, secretEnvironment)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence({
mockSecretEnvironmentsService: true,
mock,
})
expect(getItemSpy).toHaveBeenCalledWith(secretEnvironmentsKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(secretEnvironmentsKey)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
expect(loadSecretEnvironmentsFromPersistedStateFn).toHaveBeenCalledWith(
secretEnvironment
)
expect(watchDebounced).toHaveBeenCalled()
})
it(`skips schema parsing and the loading of persisted secret environments if there is no "${secretEnvironmentsKey}" key present in localStorage`, async () => {
window.localStorage.removeItem(secretEnvironmentsKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence({
mockSecretEnvironmentsService: true,
mock,
})
expect(getItemSpy).toHaveBeenCalledWith(secretEnvironmentsKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(secretEnvironmentsKey)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
expect(watchDebounced).toHaveBeenCalled()
})
it("logs an error to the console on failing to parse persisted secret environments", async () => {
await setStoreItem(secretEnvironmentsKey, "invalid-json")
console.error = vi.fn()
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(secretEnvironmentsKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(secretEnvironmentsKey)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
expect(console.error).toHaveBeenCalledWith(
`Failed parsing persisted SECRET_ENVIRONMENTS:`,
await getStoreItem(secretEnvironmentsKey)
)
expect(watchDebounced).toHaveBeenCalledWith(
expect.any(Object),
expect.any(Function),
{ debounce: 500 }
)
})
})
describe("setup WebSocket persistence", () => {
// Key read from localStorage across test cases
const wsRequestKey = `${STORE_NAMESPACE}:${STORE_KEYS.WEBSOCKET}`
it(`shows an error and sets the entry as a backup in localStorage if "${wsRequestKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `WebsocketRequest`
const request = {
...WEBSOCKET_REQUEST_MOCK,
// `protocols` -> `[]`
protocols: {},
}
await setStoreItem(wsRequestKey, request)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(wsRequestKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(wsRequestKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${wsRequestKey}-backup`,
expect.stringContaining(JSON.stringify(request))
)
})
it(`schema parsing succeeds if there is no "${wsRequestKey}" key present in localStorage where the fallback of "null" is chosen`, async () => {
window.localStorage.removeItem(wsRequestKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(wsRequestKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(wsRequestKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
})
it(`reads the "${wsRequestKey}" entry from localStorage, sets it as the new request, subscribes to the "WSSessionStore" and updates localStorage entries`, async () => {
vi.mock("~/newstore/WebSocketSession", () => {
return {
setWSRequest: vi.fn(),
WSRequest$: {
subscribe: vi.fn(),
},
}
})
const request = WEBSOCKET_REQUEST_MOCK
await setStoreItem(wsRequestKey, request)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(wsRequestKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(wsRequestKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
expect(setWSRequest).toHaveBeenCalledWith(request)
expect(WSRequest$.subscribe).toHaveBeenCalledWith(expect.any(Function))
})
})
describe("setup Socket.IO persistence", () => {
// Key read from localStorage across test cases
const sioRequestKey = `${STORE_NAMESPACE}:${STORE_KEYS.SOCKETIO}`
it(`shows an error and sets the entry as a backup in localStorage if "${sioRequestKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `SocketIORequest`
const request = {
...SOCKET_IO_REQUEST_MOCK,
// `v` -> `version: v4`
v: "4",
}
await setStoreItem(sioRequestKey, request)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(sioRequestKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(sioRequestKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${sioRequestKey}-backup`,
expect.stringContaining(JSON.stringify(request))
)
})
it(`schema parsing succeeds if there is no "${sioRequestKey}" key present in localStorage where the fallback of "null" is chosen`, async () => {
window.localStorage.removeItem(sioRequestKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(sioRequestKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(sioRequestKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
})
it(`reads the "${sioRequestKey}" entry from localStorage, sets it as the new request, subscribes to the "SIOSessionStore" and updates localStorage entries`, async () => {
vi.mock("~/newstore/SocketIOSession", () => {
return {
setSIORequest: vi.fn(),
SIORequest$: {
subscribe: vi.fn(),
},
}
})
const request = SOCKET_IO_REQUEST_MOCK
await setStoreItem(sioRequestKey, request)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(sioRequestKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(sioRequestKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
expect(setSIORequest).toHaveBeenCalledWith(request)
expect(SIORequest$.subscribe).toHaveBeenCalledWith(expect.any(Function))
})
})
describe("setup SSE persistence", () => {
// Key read from localStorage across test cases
const sseRequestKey = `${STORE_NAMESPACE}:${STORE_KEYS.SSE}`
it(`shows an error and sets the entry as a backup in localStorage if "${sseRequestKey}" read from localStorage doesn't match the versioned schema`, async () => {
// Invalid shape for `SSERequest`
const request = {
...SSE_REQUEST_MOCK,
// `url` -> `endpoint`
url: "https://express-eventsource.herokuapp.com/events",
}
await setStoreItem(sseRequestKey, request)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(sseRequestKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(sseRequestKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${sseRequestKey}-backup`,
expect.stringContaining(JSON.stringify(request))
)
})
it(`schema parsing succeeds if there is no "${sseRequestKey}" key present in localStorage where the fallback of "null" is chosen`, async () => {
window.localStorage.removeItem(sseRequestKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(sseRequestKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(sseRequestKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
})
it(`reads the "${sseRequestKey}" entry from localStorage, sets it as the new request, subscribes to the "SSESessionStore" and updates localStorage entries`, async () => {
vi.mock("~/newstore/SSESession", () => {
return {
setSSERequest: vi.fn(),
SSERequest$: {
subscribe: vi.fn(),
},
}
})
const request = SSE_REQUEST_MOCK
await setStoreItem(sseRequestKey, request)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(sseRequestKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(sseRequestKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
expect(setSSERequest).toHaveBeenCalledWith(request)
expect(SSERequest$.subscribe).toHaveBeenCalledWith(expect.any(Function))
})
})
describe("setup MQTT persistence", () => {
// Key read from localStorage across test cases
const mqttRequestKey = `${STORE_NAMESPACE}:${STORE_KEYS.MQTT}`
it(`shows an error and sets the entry as a backup in localStorage if "${mqttRequestKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `MQTTRequest`
const request = {
...MQTT_REQUEST_MOCK,
// `url` -> `endpoint`
url: "wss://test.mosquitto.org:8081",
}
await setStoreItem(mqttRequestKey, request)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(mqttRequestKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(mqttRequestKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${mqttRequestKey}-backup`,
expect.stringContaining(JSON.stringify(request))
)
})
it(`schema parsing succeeds if there is no "${mqttRequestKey}" key present in localStorage where the fallback of "null" is chosen`, async () => {
window.localStorage.removeItem(mqttRequestKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(mqttRequestKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(mqttRequestKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
})
it(`reads the ${mqttRequestKey}" entry from localStorage, sets it as the new request, subscribes to the "MQTTSessionStore" and updates localStorage entries`, async () => {
vi.mock("~/newstore/MQTTSession", () => {
return {
setMQTTRequest: vi.fn(),
MQTTRequest$: {
subscribe: vi.fn(),
},
}
})
const request = MQTT_REQUEST_MOCK
await setStoreItem(mqttRequestKey, request)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(mqttRequestKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(mqttRequestKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
expect(setMQTTRequest).toHaveBeenCalledWith(request)
expect(MQTTRequest$.subscribe).toHaveBeenCalledWith(
expect.any(Function)
)
})
})
describe("setup global environments persistence", () => {
// Key read from localStorage across test cases
const globalEnvKey = `${STORE_NAMESPACE}:${STORE_KEYS.GLOBAL_ENV}`
it(`shows an error and sets the entry as a backup in localStorage if "${globalEnvKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `globalEnv`
const globalEnv = [
{
// `key` -> `string`
key: 1,
value: "test-value",
},
]
await setStoreItem(globalEnvKey, globalEnv)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(globalEnvKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(globalEnvKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${globalEnvKey}-backup`,
expect.stringContaining(JSON.stringify(globalEnv))
)
})
it(`schema parsing succeeds if there is no "${globalEnvKey}" key present in localStorage where the fallback of "[]" is chosen`, async () => {
window.localStorage.removeItem(globalEnvKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(globalEnvKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(globalEnvKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
})
it(`reads the "globalEnv" entry from localStorage, dispatches the new value, subscribes to the "environmentsStore" and updates localStorage entries`, async () => {
const globalEnv = GLOBAL_ENV_MOCK
await setStoreItem(globalEnvKey, globalEnv)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(globalEnvKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(globalEnvKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringContaining('"schemaVersion":1')
)
expect(setGlobalEnvVariables).toHaveBeenCalledWith(globalEnv)
expect(globalEnv$.subscribe).toHaveBeenCalledWith(expect.any(Function))
})
})
describe("setup GQL tabs persistence", () => {
// Key read from localStorage across test cases
const gqlTabStateKey = `${STORE_NAMESPACE}:${STORE_KEYS.GQL_TABS}`
const loadTabsFromPersistedStateFn = vi.fn()
const mock = { loadTabsFromPersistedState: loadTabsFromPersistedStateFn }
it(`shows an error and sets the entry as a backup in localStorage if "${gqlTabStateKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `gqlTabState`
// `lastActiveTabID` -> `string`
const gqlTabState = { ...GQL_TAB_STATE_MOCK, lastActiveTabID: 1234 }
await setStoreItem(gqlTabStateKey, gqlTabState)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(gqlTabStateKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(gqlTabStateKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${gqlTabStateKey}-backup`,
expect.stringContaining(JSON.stringify(gqlTabState))
)
})
it(`skips schema parsing and the loading of persisted tabs if there is no "${gqlTabStateKey}" key present in localStorage`, async () => {
window.localStorage.removeItem(gqlTabStateKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence({ mockGQLTabService: true, mock })
expect(getItemSpy).toHaveBeenCalledWith(gqlTabStateKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(gqlTabStateKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringMatching(/"schemaVersion":1/)
)
expect(loadTabsFromPersistedStateFn).not.toHaveBeenCalled()
expect(watchDebounced).toHaveBeenCalled()
})
it("loads tabs from the state persisted in localStorage and sets watcher for `persistableTabState`", async () => {
const tabState = GQL_TAB_STATE_MOCK
await setStoreItem(gqlTabStateKey, tabState)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence({ mockGQLTabService: true, mock })
expect(getItemSpy).toHaveBeenCalledWith(gqlTabStateKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(gqlTabStateKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringMatching(/"schemaVersion":1/)
)
expect(loadTabsFromPersistedStateFn).toHaveBeenCalledWith(tabState)
expect(watchDebounced).toHaveBeenCalledWith(
expect.any(Object),
expect.any(Function),
{ debounce: 500, deep: true }
)
})
it("logs an error to the console on failing to parse persisted gql tab state", async () => {
await setStoreItem(gqlTabStateKey, "invalid-json")
console.error = vi.fn()
const getItemSpy = spyOnGetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(gqlTabStateKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(gqlTabStateKey)
expect(console.error).toHaveBeenCalledWith(
`Failed parsing persisted GQL_TABS:`,
await getStoreItem(gqlTabStateKey)
)
expect(watchDebounced).toHaveBeenCalledWith(
expect.any(Object),
expect.any(Function),
{ debounce: 500, deep: true }
)
})
})
describe("setup REST tabs persistence", () => {
// Key read from localStorage across test cases
const restTabStateKey = `${STORE_NAMESPACE}:${STORE_KEYS.REST_TABS}`
const loadTabsFromPersistedStateFn = vi.fn()
const mock = { loadTabsFromPersistedState: loadTabsFromPersistedStateFn }
it(`shows an error and sets the entry as a backup in localStorage if "${restTabStateKey}" read from localStorage doesn't match the schema`, async () => {
// Invalid shape for `restTabState`
// `lastActiveTabID` -> `string`
const restTabState = { ...REST_TAB_STATE_MOCK, lastActiveTabID: 1234 }
await setStoreItem(restTabStateKey, restTabState)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(restTabStateKey)
expect(toastErrorFn).toHaveBeenCalledWith(
expect.stringContaining(restTabStateKey)
)
expect(setItemSpy).toHaveBeenCalledWith(
`${restTabStateKey}-backup`,
expect.stringContaining(JSON.stringify(restTabState))
)
})
it(`skips schema parsing and the loading of persisted tabs if there is no "${restTabStateKey}" key present in localStorage`, async () => {
window.localStorage.removeItem(restTabStateKey)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence({ mockRESTTabService: true, mock })
expect(getItemSpy).toHaveBeenCalledWith(restTabStateKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(restTabStateKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringMatching(/"schemaVersion":1/)
)
expect(loadTabsFromPersistedStateFn).not.toHaveBeenCalled()
expect(watchDebounced).toHaveBeenCalled()
})
it("loads tabs from the state persisted in localStorage and sets watcher for `persistableTabState`", async () => {
const tabState = REST_TAB_STATE_MOCK
await setStoreItem(restTabStateKey, tabState)
const getItemSpy = spyOnGetItem()
const setItemSpy = spyOnSetItem()
await invokeSetupLocalPersistence({ mockRESTTabService: true, mock })
expect(getItemSpy).toHaveBeenCalledWith(restTabStateKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(restTabStateKey)
expect(setItemSpy).toHaveBeenCalledTimes(1)
expect(setItemSpy).toHaveBeenCalledWith(
schemaVersionKey,
expect.stringMatching(/"schemaVersion":1/)
)
expect(loadTabsFromPersistedStateFn).toHaveBeenCalledWith(tabState)
expect(watchDebounced).toHaveBeenCalledWith(
expect.any(Object),
expect.any(Function),
{ debounce: 500, deep: true }
)
})
it("logs an error to the console on failing to parse persisted rest tab state", async () => {
await setStoreItem(restTabStateKey, "invalid-json")
console.error = vi.fn()
const getItemSpy = spyOnGetItem()
await invokeSetupLocalPersistence()
expect(getItemSpy).toHaveBeenCalledWith(restTabStateKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(restTabStateKey)
expect(console.error).toHaveBeenCalledWith(
`Failed parsing persisted REST_TABS:`,
await getStoreItem(restTabStateKey)
)
expect(watchDebounced).toHaveBeenCalledWith(
expect.any(Object),
expect.any(Function),
{ debounce: 500, deep: true }
)
})
})
})
it("`setLocalConfig` method sets a value in localStorage", async () => {
const testKey = "test-key"
const testValue = "test-value"
const setItemSpy = spyOnSetItem()
const service = bindPersistenceService()
await service.setLocalConfig(testKey, testValue)
expect(setItemSpy).toHaveBeenCalledWith(
`${STORE_NAMESPACE}:${testKey}`,
expect.stringContaining(testValue)
)
})
it("`getLocalConfig` method gets a value from localStorage", async () => {
const testKey = "test-key"
const testValue = "test-value"
const setItemSpy = spyOnSetItem()
const getItemSpy = spyOnGetItem()
const service = bindPersistenceService()
await service.setLocalConfig(testKey, testValue)
const retrievedValue = await service.getLocalConfig(testKey)
expect(setItemSpy).toHaveBeenCalledWith(
`${STORE_NAMESPACE}:${testKey}`,
expect.stringContaining(testValue)
)
expect(getItemSpy).toHaveBeenCalledWith(`${STORE_NAMESPACE}:${testKey}`)
expect(retrievedValue).toBe(testValue)
})
it("`removeLocalConfig` method clears a value in localStorage", async () => {
const testKey = "test-key"
const testValue = "test-value"
const setItemSpy = spyOnSetItem()
const removeItemSpy = spyOnRemoveItem()
const service = bindPersistenceService()
await service.setLocalConfig(testKey, testValue)
await service.removeLocalConfig(testKey)
expect(setItemSpy).toHaveBeenCalledWith(
`${STORE_NAMESPACE}:${testKey}`,
expect.stringContaining(testValue)
)
expect(removeItemSpy).toHaveBeenCalledWith(`${STORE_NAMESPACE}:${testKey}`)
})
it("`set` method sets a value in localStorage", async () => {
const testKey = "temp"
const testValue = "test-value"
const setItemSpy = spyOnSetItem()
const service = bindPersistenceService()
await service.set(testKey, testValue)
expect(setItemSpy).toHaveBeenCalledWith(
`${STORE_NAMESPACE}:${testKey}`,
expect.stringContaining(testValue)
)
})
it("`get` method gets a value from localStorage", async () => {
const testKey = "temp"
const testValue = "test-value"
const setItemSpy = spyOnSetItem()
const getItemSpy = spyOnGetItem()
const service = bindPersistenceService()
await service.set(testKey, testValue)
const retrievedValue = await service.get(testKey)
expect(setItemSpy).toHaveBeenCalledWith(
`${STORE_NAMESPACE}:${testKey}`,
expect.stringContaining(testValue)
)
expect(getItemSpy).toHaveBeenCalledWith(`${STORE_NAMESPACE}:${testKey}`)
expect(retrievedValue).toStrictEqual(E.right(testValue))
})
it("`remove` method clears a value in localStorage", async () => {
const testKey = "temp"
const testValue = "test-value"
const setItemSpy = spyOnSetItem()
const removeItemSpy = spyOnRemoveItem()
const service = bindPersistenceService()
await service.set(testKey, testValue)
await service.remove(testKey)
expect(setItemSpy).toHaveBeenCalledWith(
`${STORE_NAMESPACE}:${testKey}`,
expect.stringContaining(testValue)
)
expect(removeItemSpy).toHaveBeenCalledWith(`${STORE_NAMESPACE}:${testKey}`)
})
})