fix(js-sandbox): resolve environment variable fallback behavior (#5439)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: nivedin <nivedinp@gmail.com> Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
453b5fc088
commit
9a4e5a7f7e
4 changed files with 1005 additions and 34 deletions
|
|
@ -0,0 +1,864 @@
|
|||
/**
|
||||
* Environment Variable Fallback Behavior Tests
|
||||
*
|
||||
* Tests the fallback from currentValue to initialValue when currentValue is empty
|
||||
* across pm.environment and hopp.env namespaces.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from "vitest"
|
||||
import { runTest } from "~/utils/test-helpers"
|
||||
|
||||
// All namespaces share the same environment variable fallback implementation
|
||||
// Testing: pm.environment (active scope) and hopp.env
|
||||
// Note: pm.globals uses same implementation as pm.environment, just different scope
|
||||
const NAMESPACES = [
|
||||
{
|
||||
name: "pm.environment",
|
||||
get: "pm.environment.get",
|
||||
set: "pm.environment.set",
|
||||
},
|
||||
{ name: "hopp.env", get: "hopp.env.get", set: "hopp.env.set" },
|
||||
] as const
|
||||
|
||||
describe("Environment Variable Fallback Behavior - All Namespaces", () => {
|
||||
describe.each(NAMESPACES)(
|
||||
"$name - currentValue empty string fallback",
|
||||
({ get }) => {
|
||||
test("should fallback to initialValue when currentValue is empty string", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [
|
||||
{
|
||||
key: "testVar",
|
||||
currentValue: "", // Empty string
|
||||
initialValue: "fallback_value",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = ${get}("testVar")
|
||||
pm.test("should get fallback value", () => {
|
||||
pm.expect(value).to.equal("fallback_value")
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
test("should return empty string when both currentValue and initialValue are empty", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [
|
||||
{
|
||||
key: "testVar",
|
||||
currentValue: "",
|
||||
initialValue: "",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = ${get}("testVar")
|
||||
pm.test("should get empty string", () => {
|
||||
pm.expect(value).to.equal("")
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
describe.each(NAMESPACES)(
|
||||
"$name - currentValue undefined/null fallback",
|
||||
({ get }) => {
|
||||
// Note: These tests check GET behavior with null/undefined in initial state.
|
||||
// Setting null via pm.environment.set(key, null) is currently NOT supported.
|
||||
// These tests validate fallback logic when null/undefined exists in the env state,
|
||||
// which can occur through other means (e.g., test setup, external sources).
|
||||
|
||||
test("should fallback to initialValue when currentValue is undefined", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [
|
||||
{
|
||||
key: "testVar",
|
||||
currentValue: undefined,
|
||||
initialValue: "fallback_value",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = ${get}("testVar")
|
||||
pm.test("should get fallback value", () => {
|
||||
pm.expect(value).to.equal("fallback_value")
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
test("should fallback to initialValue when currentValue is null", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [
|
||||
{
|
||||
key: "testVar",
|
||||
currentValue: null,
|
||||
initialValue: "fallback_value",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = ${get}("testVar")
|
||||
pm.test("should get fallback value for null", () => {
|
||||
pm.expect(value).to.equal("fallback_value")
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
describe.each(NAMESPACES)(
|
||||
"$name - falsy values should NOT fallback",
|
||||
({ get }) => {
|
||||
test("should use currentValue when it is 0 (not fallback)", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [
|
||||
{
|
||||
key: "testVar",
|
||||
currentValue: 0,
|
||||
initialValue: 100,
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = ${get}("testVar")
|
||||
pm.test("should get 0, not fallback to 100", () => {
|
||||
pm.expect(value).to.equal(0)
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
test("should use currentValue when it is false (not fallback)", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [
|
||||
{
|
||||
key: "testVar",
|
||||
currentValue: false,
|
||||
initialValue: true,
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = ${get}("testVar")
|
||||
pm.test("should get false, not fallback to true", () => {
|
||||
pm.expect(value).to.equal(false)
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
test("should use currentValue when it is empty array (not fallback)", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [
|
||||
{
|
||||
key: "testVar",
|
||||
currentValue: [],
|
||||
initialValue: [1, 2, 3],
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = ${get}("testVar")
|
||||
pm.test("should get empty array, not fallback", () => {
|
||||
pm.expect(value).to.be.an("array")
|
||||
pm.expect(value.length).to.equal(0)
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
test("should use currentValue when it is empty object (not fallback)", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [
|
||||
{
|
||||
key: "testVar",
|
||||
currentValue: {},
|
||||
initialValue: { key: "value" },
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = ${get}("testVar")
|
||||
pm.test("should get empty object, not fallback", () => {
|
||||
pm.expect(value).to.be.an("object")
|
||||
pm.expect(Object.keys(value).length).to.equal(0)
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// NaN test - only for pm namespace which preserves non-string types
|
||||
describe("pm.environment - NaN handling", () => {
|
||||
test("should use currentValue when it is NaN (not fallback)", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [
|
||||
{
|
||||
key: "testVar",
|
||||
currentValue: NaN,
|
||||
initialValue: 100,
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = pm.environment.get("testVar")
|
||||
pm.test("should get NaN, not fallback to 100", () => {
|
||||
pm.expect(Number.isNaN(value)).to.be.true
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe.each(NAMESPACES)(
|
||||
"$name - complex type preservation with fallback",
|
||||
({ get }) => {
|
||||
test("should fallback to initialValue array when currentValue is empty", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [
|
||||
{
|
||||
key: "users",
|
||||
currentValue: "",
|
||||
initialValue: [
|
||||
{ id: 1, name: "Alice" },
|
||||
{ id: 2, name: "Bob" },
|
||||
],
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = ${get}("users")
|
||||
pm.test("should get array from initialValue", () => {
|
||||
pm.expect(value).to.be.an("array")
|
||||
pm.expect(value.length).to.equal(2)
|
||||
pm.expect(value[0].name).to.equal("Alice")
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
test("should fallback to initialValue object when currentValue is null", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [
|
||||
{
|
||||
key: "config",
|
||||
currentValue: null,
|
||||
initialValue: { debug: true, maxRetries: 3 },
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = ${get}("config")
|
||||
pm.test("should get object from initialValue", () => {
|
||||
pm.expect(value).to.be.an("object")
|
||||
pm.expect(value.debug).to.equal(true)
|
||||
pm.expect(value.maxRetries).to.equal(3)
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
describe.each(NAMESPACES)(
|
||||
"$name - global vs selected scope fallback",
|
||||
({ name, get }) => {
|
||||
test("should fallback to initialValue in global scope", async () => {
|
||||
const envs = {
|
||||
global: [
|
||||
{
|
||||
key: "globalVar",
|
||||
currentValue: "",
|
||||
initialValue: "global_fallback",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [],
|
||||
}
|
||||
|
||||
// Use hopp.env.global.get for hopp, pm.globals.get for pm
|
||||
const globalGet =
|
||||
name === "pm.environment" ? "pm.globals.get" : "hopp.env.global.get"
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = ${globalGet}("globalVar")
|
||||
pm.test("should get global fallback value", () => {
|
||||
pm.expect(value).to.equal("global_fallback")
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
test("should fallback in selected scope even when global has a value", async () => {
|
||||
const envs = {
|
||||
global: [
|
||||
{
|
||||
key: "sharedVar",
|
||||
currentValue: "global_value",
|
||||
initialValue: "global_initial",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [
|
||||
{
|
||||
key: "sharedVar",
|
||||
currentValue: "", // Empty in selected
|
||||
initialValue: "selected_initial",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = ${get}("sharedVar")
|
||||
pm.test("should get selected scope's initialValue, not global", () => {
|
||||
pm.expect(value).to.equal("selected_initial")
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
describe.each(NAMESPACES)(
|
||||
"$name - runtime modification and fallback",
|
||||
({ get, set }) => {
|
||||
test("should use newly set value, not fallback to initialValue", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [
|
||||
{
|
||||
key: "testVar",
|
||||
currentValue: "",
|
||||
initialValue: "initial",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
// First, verify it falls back to initialValue
|
||||
const before = ${get}("testVar")
|
||||
pm.test("before set: should get initial value", () => {
|
||||
pm.expect(before).to.equal("initial")
|
||||
})
|
||||
|
||||
// Now set a new value
|
||||
${set}("testVar", "runtime_value")
|
||||
|
||||
// Verify it uses the newly set value
|
||||
const after = ${get}("testVar")
|
||||
pm.test("after set: should get runtime value", () => {
|
||||
pm.expect(after).to.equal("runtime_value")
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
test("should fallback when currentValue is set to empty string at runtime", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [
|
||||
{
|
||||
key: "testVar",
|
||||
currentValue: "original",
|
||||
initialValue: "fallback",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
// First, verify original value
|
||||
const before = ${get}("testVar")
|
||||
pm.test("before: should get original value", () => {
|
||||
pm.expect(before).to.equal("original")
|
||||
})
|
||||
|
||||
// Set to empty string
|
||||
${set}("testVar", "")
|
||||
|
||||
// Should now fall back to initialValue
|
||||
const after = ${get}("testVar")
|
||||
pm.test("after setting empty: should fallback to initial", () => {
|
||||
pm.expect(after).to.equal("fallback")
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// PM namespace returns undefined for non-existent keys
|
||||
describe("pm.environment - non-existent variable behavior", () => {
|
||||
test("should return undefined for non-existent variable (not fallback)", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = pm.environment.get("nonExistent")
|
||||
pm.test("should get undefined for non-existent", () => {
|
||||
pm.expect(value).to.be.undefined
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Hopp namespace returns null for non-existent keys
|
||||
describe("hopp.env - non-existent variable behavior", () => {
|
||||
test("should return null for non-existent variable (not fallback)", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = hopp.env.get("nonExistent")
|
||||
pm.test("should get null for non-existent", () => {
|
||||
pm.expect(value).to.be.null
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Additional tests for pm.variables which searches both scopes
|
||||
describe("pm.variables - Fallback Behavior Across Scopes", () => {
|
||||
test("should fallback to initialValue in selected scope first", async () => {
|
||||
const envs = {
|
||||
global: [
|
||||
{
|
||||
key: "sharedVar",
|
||||
currentValue: "global_value",
|
||||
initialValue: "global_initial",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [
|
||||
{
|
||||
key: "sharedVar",
|
||||
currentValue: "",
|
||||
initialValue: "selected_initial",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = pm.variables.get("sharedVar")
|
||||
pm.test("should get selected scope's initialValue", () => {
|
||||
pm.expect(value).to.equal("selected_initial")
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
test("should search global scope when selected scope variable doesn't exist", async () => {
|
||||
const envs = {
|
||||
global: [
|
||||
{
|
||||
key: "globalOnly",
|
||||
currentValue: "",
|
||||
initialValue: "global_fallback",
|
||||
secret: false,
|
||||
},
|
||||
],
|
||||
selected: [],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = pm.variables.get("globalOnly")
|
||||
pm.test("should get global scope's initialValue", () => {
|
||||
pm.expect(value).to.equal("global_fallback")
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
test("should return undefined when variable doesn't exist in either scope", async () => {
|
||||
const envs = {
|
||||
global: [],
|
||||
selected: [],
|
||||
}
|
||||
|
||||
const result = await runTest(
|
||||
`
|
||||
const value = pm.variables.get("nonExistent")
|
||||
pm.test("should get undefined", () => {
|
||||
pm.expect(value).to.be.undefined
|
||||
})
|
||||
`,
|
||||
envs
|
||||
)()
|
||||
|
||||
expect(result).toEqualRight(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
children: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
expectResults: expect.arrayContaining([
|
||||
expect.objectContaining({ status: "pass" }),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -13,6 +13,12 @@ import { createPmNamespaceMethods } from "../namespaces/pm-namespace"
|
|||
import { createPwNamespaceMethods } from "../namespaces/pw-namespace"
|
||||
|
||||
type BaseInputsConfig = {
|
||||
/**
|
||||
* Environment variables typed as TestResult["envs"] for external API compatibility.
|
||||
* At runtime, this will be mutated to contain SandboxValue types (arrays, objects, etc.)
|
||||
* during script execution to support PM namespace compatibility.
|
||||
* See `getSharedEnvMethods()` for detailed explanation of the type flow.
|
||||
*/
|
||||
envs: TestResult["envs"]
|
||||
request: HoppRESTRequest
|
||||
cookies: Cookie[] | null
|
||||
|
|
|
|||
|
|
@ -22,11 +22,21 @@ type SandboxFunction = ReturnType<typeof defineSandboxFn>
|
|||
* - Return values sent back to the sandbox
|
||||
* - PM namespace compatibility (preserves non-string types like arrays, objects)
|
||||
*
|
||||
* Supported types for environment variable values:
|
||||
* - Primitives: string, number, boolean, null, undefined
|
||||
* - Objects: plain objects, arrays (recursively containing these types)
|
||||
* - Unsupported: Functions, Symbols, and other non-serializable types
|
||||
*
|
||||
* Note: Typed as `any` because this type is used in multiple contexts:
|
||||
* 1. Environment variable storage (supports primitives, objects, arrays)
|
||||
* 2. Function parameters from user scripts (requires runtime validation)
|
||||
* 3. Internal object properties (may include QuickJS handles)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Function accepting values from user scripts
|
||||
* const envSetAny = (key: SandboxValue, value: SandboxValue) => {
|
||||
* // Runtime validation
|
||||
* // Runtime validation required since type is `any`
|
||||
* if (typeof key !== "string") throw new Error("Expected string key")
|
||||
* // ... handle value
|
||||
* }
|
||||
|
|
@ -85,23 +95,67 @@ export type TestDescriptor = {
|
|||
children: TestDescriptor[]
|
||||
}
|
||||
|
||||
// Representation of a transformed state for environment variables in the sandbox
|
||||
type TransformedEnvironmentVariable = {
|
||||
/**
|
||||
* Internal representation of environment variables within the sandbox runtime.
|
||||
*
|
||||
* Values can be complex types (arrays, objects) while executing scripts.
|
||||
* This type is exported for internal use within js-sandbox but should NOT
|
||||
* be used by consuming packages - use `EnvironmentVariable` instead.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export type SandboxEnvironmentVariable = {
|
||||
key: string
|
||||
currentValue: string
|
||||
initialValue: string
|
||||
currentValue: SandboxValue // Can be arrays, objects, primitives
|
||||
initialValue: SandboxValue
|
||||
secret: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the result of a test script execution
|
||||
* Internal representation of the envs structure during sandbox execution.
|
||||
* This is what's used internally, before serialization to TestResult["envs"].
|
||||
*
|
||||
* At runtime, environment variables are stored with SandboxValue types to support
|
||||
* PM namespace compatibility (arrays, objects, etc.). The serialization to strings
|
||||
* happens only when getUpdatedEnvs() is called at the end of script execution.
|
||||
*
|
||||
* Note: This type is structurally compatible with TestResult["envs"] at runtime,
|
||||
* but TypeScript sees them as different types due to the SandboxValue vs string
|
||||
* difference. Use type assertions when converting between them.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export type SandboxEnvs = {
|
||||
global: SandboxEnvironmentVariable[]
|
||||
selected: SandboxEnvironmentVariable[]
|
||||
}
|
||||
|
||||
/**
|
||||
* External representation of environment variables at the API boundary.
|
||||
*
|
||||
* All values are serialized to strings when crossing the sandbox boundary
|
||||
* via getUpdatedEnvs() which calls JSON.stringify() on complex types.
|
||||
*
|
||||
* This is what consuming packages (hoppscotch-common, cli, etc.) receive.
|
||||
*/
|
||||
export type EnvironmentVariable = {
|
||||
key: string
|
||||
currentValue: string // Always string after serialization
|
||||
initialValue: string // Always string after serialization
|
||||
secret: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the result of a test script execution.
|
||||
*
|
||||
* Note: envs contain EnvironmentVariable (strings) not SandboxValue,
|
||||
* because values are serialized when leaving the sandbox.
|
||||
*/
|
||||
export type TestResult = {
|
||||
tests: TestDescriptor[]
|
||||
envs: {
|
||||
global: TransformedEnvironmentVariable[]
|
||||
selected: TransformedEnvironmentVariable[]
|
||||
global: EnvironmentVariable[]
|
||||
selected: EnvironmentVariable[]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +334,8 @@ export interface BaseInputs
|
|||
cookieGetAll: SandboxFunction
|
||||
cookieDelete: SandboxFunction
|
||||
cookieClear: SandboxFunction
|
||||
getUpdatedEnvs: () => SandboxValue
|
||||
// Returns serialized env vars (SandboxValue -> string conversion happens here)
|
||||
getUpdatedEnvs: () => TestResult["envs"]
|
||||
getUpdatedCookies: () => Cookie[] | null
|
||||
[key: string]: SandboxValue // Index signature for dynamic namespace properties
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import { cloneDeep } from "lodash-es"
|
|||
|
||||
import {
|
||||
Expectation,
|
||||
GlobalEnvItem,
|
||||
SelectedEnvItem,
|
||||
TestDescriptor,
|
||||
TestResult,
|
||||
SandboxValue,
|
||||
SandboxEnvironmentVariable,
|
||||
SandboxEnvs,
|
||||
} from "../types"
|
||||
|
||||
export type EnvSource = "active" | "global" | "all"
|
||||
|
|
@ -26,45 +26,45 @@ export type EnvAPIOptions = {
|
|||
|
||||
const getEnv = (
|
||||
envName: string,
|
||||
envs: TestResult["envs"],
|
||||
envs: SandboxEnvs,
|
||||
options = { source: "all" }
|
||||
) => {
|
||||
if (options.source === "active") {
|
||||
return O.fromNullable(
|
||||
envs.selected.find((x: SelectedEnvItem) => x.key === envName)
|
||||
envs.selected.find((x: SandboxEnvironmentVariable) => x.key === envName)
|
||||
)
|
||||
}
|
||||
|
||||
if (options.source === "global") {
|
||||
return O.fromNullable(
|
||||
envs.global.find((x: GlobalEnvItem) => x.key === envName)
|
||||
envs.global.find((x: SandboxEnvironmentVariable) => x.key === envName)
|
||||
)
|
||||
}
|
||||
|
||||
return O.fromNullable(
|
||||
envs.selected.find((x: SelectedEnvItem) => x.key === envName) ??
|
||||
envs.global.find((x: GlobalEnvItem) => x.key === envName)
|
||||
envs.selected.find((x: SandboxEnvironmentVariable) => x.key === envName) ??
|
||||
envs.global.find((x: SandboxEnvironmentVariable) => x.key === envName)
|
||||
)
|
||||
}
|
||||
|
||||
const findEnvIndex = (
|
||||
envName: string,
|
||||
envList: SelectedEnvItem[] | GlobalEnvItem[]
|
||||
envList: SandboxEnvironmentVariable[]
|
||||
): number => {
|
||||
return envList.findIndex(
|
||||
(envItem: SelectedEnvItem) => envItem.key === envName
|
||||
(envItem: SandboxEnvironmentVariable) => envItem.key === envName
|
||||
)
|
||||
}
|
||||
|
||||
const setEnv = (
|
||||
envName: string,
|
||||
envValue: SandboxValue,
|
||||
envs: TestResult["envs"],
|
||||
envs: SandboxEnvs,
|
||||
options: { setInitialValue?: boolean; source: EnvSource } = {
|
||||
setInitialValue: false,
|
||||
source: "all",
|
||||
}
|
||||
): TestResult["envs"] => {
|
||||
): SandboxEnvs => {
|
||||
const { global, selected } = envs
|
||||
|
||||
const indexInSelected = findEnvIndex(envName, selected)
|
||||
|
|
@ -108,9 +108,9 @@ const setEnv = (
|
|||
|
||||
const unsetEnv = (
|
||||
envName: string,
|
||||
envs: TestResult["envs"],
|
||||
envs: SandboxEnvs,
|
||||
options = { source: "all" }
|
||||
): TestResult["envs"] => {
|
||||
): SandboxEnvs => {
|
||||
const { global, selected } = envs
|
||||
|
||||
const indexInSelected = findEnvIndex(envName, selected)
|
||||
|
|
@ -156,7 +156,7 @@ export function getSharedEnvMethods(
|
|||
}
|
||||
}
|
||||
pmSetAny: (key: string, value: SandboxValue, options?: EnvAPIOptions) => void
|
||||
updatedEnvs: TestResult["envs"]
|
||||
updatedEnvs: SandboxEnvs
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -179,14 +179,36 @@ export function getSharedEnvMethods(
|
|||
resolve: (key: string) => string
|
||||
}
|
||||
}
|
||||
updatedEnvs: TestResult["envs"]
|
||||
updatedEnvs: SandboxEnvs
|
||||
}
|
||||
|
||||
export function getSharedEnvMethods(
|
||||
envs: TestResult["envs"],
|
||||
isHoppNamespace = false
|
||||
): unknown {
|
||||
let updatedEnvs = envs
|
||||
/**
|
||||
* Type assertion explanation:
|
||||
*
|
||||
* The `envs` parameter is typed as `TestResult["envs"]` (with string values) for external API
|
||||
* compatibility, but at runtime it contains `SandboxValue` types during script execution.
|
||||
*
|
||||
* Data flow:
|
||||
* 1. Entry: External caller passes envs with string values
|
||||
* { global: [{ key: "count", currentValue: "5", initialValue: "0" }], selected: [] }
|
||||
*
|
||||
* 2. Execution: Scripts mutate with complex types (PM namespace compatibility)
|
||||
* pm.environment.set("users", [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }])
|
||||
* pm.environment.set("config", { debug: true, maxRetries: 3 })
|
||||
* // Now: currentValue is an array/object, not a string!
|
||||
*
|
||||
* 3. Exit: getUpdatedEnvs() serializes back to strings via JSON.stringify()
|
||||
* { global: [{ key: "users", currentValue: "[{...}]", initialValue: "[]" }], ... }
|
||||
*
|
||||
* The cast acknowledges that during execution (steps 1-3), the runtime type is SandboxEnvs,
|
||||
* even though the declared type is TestResult["envs"] for API boundary compatibility.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
let updatedEnvs = envs as unknown as SandboxEnvs
|
||||
|
||||
const envGetFn = (
|
||||
key: unknown,
|
||||
|
|
@ -200,7 +222,19 @@ export function getSharedEnvMethods(
|
|||
getEnv(key, updatedEnvs, options),
|
||||
O.fold(
|
||||
() => (options.fallbackToNull ? null : undefined),
|
||||
(env) => env.currentValue // Return value as-is (PM namespace preserves types)
|
||||
(env) => {
|
||||
// Get the value to use (currentValue or fallback to initialValue)
|
||||
// Treat undefined, empty string, and null as "empty" and fallback to initialValue
|
||||
const valueToUse =
|
||||
env.currentValue !== undefined &&
|
||||
env.currentValue !== "" &&
|
||||
env.currentValue !== null
|
||||
? env.currentValue
|
||||
: env.initialValue
|
||||
|
||||
// Preserve complex types (arrays, objects) for PM namespace compatibility
|
||||
return valueToUse
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -228,15 +262,27 @@ export function getSharedEnvMethods(
|
|||
E.fromOption(() => "INVALID_KEY" as const),
|
||||
|
||||
E.map((e) => {
|
||||
// Only resolve templates if the value is a string (PM namespace may have non-strings)
|
||||
if (typeof e.currentValue === "string") {
|
||||
return pipe(
|
||||
parseTemplateStringE(e.currentValue, envVars),
|
||||
E.getOrElse(() => e.currentValue)
|
||||
)
|
||||
// Get the value to use (currentValue or fallback to initialValue)
|
||||
// Treat undefined, empty string, and null as "empty" and fallback to initialValue
|
||||
const valueToUse =
|
||||
e.currentValue !== undefined &&
|
||||
e.currentValue !== "" &&
|
||||
e.currentValue !== null
|
||||
? e.currentValue
|
||||
: e.initialValue
|
||||
|
||||
// Only resolve templates for string values
|
||||
// Non-string values (arrays, objects, etc.) are returned as-is for PM namespace compatibility
|
||||
if (typeof valueToUse !== "string") {
|
||||
return valueToUse
|
||||
}
|
||||
// Return non-string values as-is (arrays, objects, null, etc.)
|
||||
return e.currentValue
|
||||
|
||||
// For string values, resolve templates
|
||||
return pipe(
|
||||
parseTemplateStringE(valueToUse, envVars),
|
||||
// If the recursive resolution failed, return the unresolved value
|
||||
E.getOrElse(() => valueToUse)
|
||||
)
|
||||
}),
|
||||
|
||||
E.getOrElseW(() => (options.fallbackToNull ? null : undefined))
|
||||
|
|
|
|||
Loading…
Reference in a new issue