diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index 116fefb09..17faa570c 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -185,7 +185,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original {formik.values.map((variable, index) => ( - + {formik.values.map((variable, index) => ( - + { + const variables = []; + const flatEnvData = flattenObject(env?.data || {}); + Object.entries(flatEnvData).forEach(([name, value]) => { + variables.push({ + uid: uuid(), + name, + value: String(value), + type: 'text', + enabled: true, + secret: false + }); + }); + + return { + uid: uuid(), + name: (env?.name && String(env.name).trim()) || `Environment ${index + 1}`, + variables + }; +}; + +/** + * Shallowly merges two flattened env data objects. + * - Keys in override replace keys in base. + * - No recursive merging. + */ +const shallowMergeFlat = (baseFlat = {}, overrideFlat = {}) => ({ ...baseFlat, ...overrideFlat }); + +/** + * Builds Bruno environments from Insomnia v5 environments. + * - Expects a single object (base env) with optional subEnvironments. + * - Creates one env for base and one env per sub using flattened, shallow-merged keys. + */ +export const buildV5Environments = (baseEnv) => { + if (!baseEnv || typeof baseEnv !== 'object') return []; + + const result = []; + + // include base as standalone + result.push(toBrunoEnv(baseEnv)); + + const subs = Array.isArray(baseEnv.subEnvironments) ? baseEnv.subEnvironments : []; + const baseFlat = flattenObject(baseEnv?.data || {}); + subs.forEach((sub, i) => { + const subFlat = flattenObject(sub?.data || {}); + const mergedFlat = shallowMergeFlat(baseFlat, subFlat); + result.push(toBrunoEnv({ name: sub?.name, data: mergedFlat }, i + 1)); + }); + return result; +}; + +/** + * Builds Bruno environments from Insomnia v4 resources. + * - Base env: parentId equals workspaceId; included as-is (flattened). + * - Sub envs: merge base (flattened) with sub (flattened) and import. + * + * Note: Insomnia supports only ONE base environment per workspace. + */ +export const buildV4Environments = (resources, workspaceId) => { + const allEnvResources = resources.filter((r) => r._type === 'environment') || []; + const envById = {}; + allEnvResources.forEach((e) => (envById[e._id] = e)); + + const isBaseEnv = (env) => env.parentId === workspaceId; + + const result = []; + + const baseEnv = allEnvResources.find(isBaseEnv); + if (baseEnv) { + result.push(toBrunoEnv(baseEnv)); + } + + // sub envs - all inherit from the single base environment + const subEnvs = allEnvResources.filter((e) => !isBaseEnv(e)); + const baseFlat = flattenObject(baseEnv?.data || {}); + subEnvs.forEach((sub, idx) => { + const subFlat = flattenObject(sub.data || {}); + const mergedFlat = shallowMergeFlat(baseFlat, subFlat); + result.push(toBrunoEnv({ name: sub.name, data: mergedFlat }, idx + 1)); + }); + + return result; +}; diff --git a/packages/bruno-converters/src/insomnia/insomnia-to-bruno.js b/packages/bruno-converters/src/insomnia/insomnia-to-bruno.js index 4968870b0..cbf2e2930 100644 --- a/packages/bruno-converters/src/insomnia/insomnia-to-bruno.js +++ b/packages/bruno-converters/src/insomnia/insomnia-to-bruno.js @@ -2,13 +2,14 @@ import each from 'lodash/each'; import get from 'lodash/get'; import jsyaml from 'js-yaml'; import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common'; +import { buildV5Environments, buildV4Environments } from './env-utils'; const parseGraphQL = (text) => { try { const graphql = JSON.parse(text); return { - query: graphql.query, + query: normalizeVariables(graphql.query), variables: JSON.stringify(graphql.variables, null, 2) }; } catch (e) { @@ -49,7 +50,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => { name, type: 'http-request', request: { - url: request.url, + url: normalizeVariables(request.url), method: request.method, auth: { mode: 'none', @@ -74,7 +75,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => { brunoRequestItem.request.headers.push({ uid: uuid(), name: header.name, - value: header.value, + value: normalizeVariables(header.value), description: header.description, enabled: !header.disabled }); @@ -84,7 +85,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => { brunoRequestItem.request.params.push({ uid: uuid(), name: param.name, - value: param.value, + value: normalizeVariables(param.value), description: param.description, type: 'query', enabled: !param.disabled @@ -95,7 +96,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => { brunoRequestItem.request.params.push({ uid: uuid(), name: param.name, - value: param.value, + value: normalizeVariables(param.value), description: '', type: 'path', enabled: true @@ -121,14 +122,14 @@ const transformInsomniaRequestItem = (request, index, allRequests) => { if (mimeType === 'application/json') { brunoRequestItem.request.body.mode = 'json'; - brunoRequestItem.request.body.json = request.body.text; + brunoRequestItem.request.body.json = normalizeVariables(request.body.text); } else if (mimeType === 'application/x-www-form-urlencoded') { brunoRequestItem.request.body.mode = 'formUrlEncoded'; each(request.body.params, (param) => { brunoRequestItem.request.body.formUrlEncoded.push({ uid: uuid(), name: param.name, - value: param.value, + value: normalizeVariables(param.value), description: param.description, enabled: !param.disabled }); @@ -140,17 +141,17 @@ const transformInsomniaRequestItem = (request, index, allRequests) => { uid: uuid(), type: 'text', name: param.name, - value: param.value, + value: normalizeVariables(param.value), description: param.description, enabled: !param.disabled }); }); } else if (mimeType === 'text/plain') { brunoRequestItem.request.body.mode = 'text'; - brunoRequestItem.request.body.text = request.body.text; + brunoRequestItem.request.body.text = normalizeVariables(request.body.text); } else if (mimeType === 'text/xml' || mimeType === 'application/xml') { brunoRequestItem.request.body.mode = 'xml'; - brunoRequestItem.request.body.xml = request.body.text; + brunoRequestItem.request.body.xml = normalizeVariables(request.body.text); } else if (mimeType === 'application/graphql') { brunoRequestItem.type = 'graphql-request'; brunoRequestItem.request.body.mode = 'graphql'; @@ -229,7 +230,7 @@ const parseInsomniaV5Collection = (data) => { // Parse environments if available if (data.environments) { - // Handle environments implementation if needed + brunoCollection.environments = buildV5Environments(data.environments); } return brunoCollection; @@ -287,6 +288,9 @@ const parseInsomniaCollection = (data) => { } brunoCollection.items = createFolderStructure(requestsAndFolders, insomniaCollection._id); + + // Build environments from resources + brunoCollection.environments = buildV4Environments(insomniaResources, insomniaCollection._id); return brunoCollection; } catch (err) { console.error('Error parsing collection:', err); diff --git a/packages/bruno-converters/src/utils/flatten.js b/packages/bruno-converters/src/utils/flatten.js new file mode 100644 index 000000000..950b4b634 --- /dev/null +++ b/packages/bruno-converters/src/utils/flatten.js @@ -0,0 +1,51 @@ +// Adapted from flat library by Hugh Kennedy (https://github.com/hughsk/flat) +// MIT License + +/** + * Recursively flattens a nested object or array into a flat object with JavaScript-style keys. + * Arrays use square bracket notation (e.g., items[0].id). + * Only primitives and null are included as values. + * + * @param {object|array} obj - The object or array to flatten. + * @param {string} [prefix] - Used internally for recursion to build the path. + * @returns {object} A flat object with JavaScript-style keys. + */ +function flattenObject(obj, prefix = '') { + // Store the final flat result + const result = {}; + + /** + * Internal recursive function to process each value. + * @param {*} value - The current value (can be object, array, primitive, or null) + * @param {string} path - The JavaScript-style key up to this point + */ + function step(value, path) { + // If value is a primitive (string, number, boolean) or null, add it to the result + if (value === null || typeof value !== 'object') { + result[path] = value; + return; + } + + // If value is an array, iterate over each item by index + if (Array.isArray(value)) { + value.forEach((item, idx) => { + // Build the next path with array index using square brackets (e.g. "items[0]") + step(item, path ? `${path}[${idx}]` : `[${idx}]`); + }); + } else { + // If value is an object, iterate over its keys + Object.entries(value).forEach(([key, val]) => { + // Build the next path with object key (e.g. "user.name") + step(val, path ? `${path}.${key}` : key); + }); + } + } + + // Start recursive flattening from the root object + step(obj, prefix); + + // Return the flat result object + return result; +} + +export { flattenObject }; diff --git a/packages/bruno-converters/tests/insomnia/env-utils.spec.js b/packages/bruno-converters/tests/insomnia/env-utils.spec.js new file mode 100644 index 000000000..7d9b26847 --- /dev/null +++ b/packages/bruno-converters/tests/insomnia/env-utils.spec.js @@ -0,0 +1,100 @@ +import { describe, it, expect } from '@jest/globals'; +import { buildV5Environments, buildV4Environments } from '../../src/insomnia/env-utils'; + +const getVar = (env, name) => { + return env.variables.find((v) => v.name === name); +}; + +describe('env-utils', () => { + describe('buildV5Environments', () => { + it('creates base and sub environments with flattened keys and shallow overrides', () => { + const environmentsNode = { + name: 'Base', + data: { + baseurl: 'https://api.example.com', + nested: { name: 'alice', roles: ['admin'] }, + numbers: [1, 2] + }, + subEnvironments: [ + { + name: 'Staging', + data: { + baseurl: 'https://staging.example.com', + nested: { name: 'bob' } + } + }, + { name: 'Dev', data: {} } + ] + }; + + const envs = buildV5Environments(environmentsNode); + expect(envs.length).toBe(3); + + const base = envs[0]; + const staging = envs[1]; + const dev = envs[2]; + + expect(base.name).toBe('Base'); + expect(getVar(base, 'baseurl')?.value).toBe('https://api.example.com'); + expect(getVar(base, 'nested.name')?.value).toBe('alice'); + expect(getVar(base, 'nested.roles[0]')?.value).toBe('admin'); + expect(getVar(base, 'numbers[1]')?.value).toBe('2'); + + expect(staging.name).toBe('Staging'); + // baseurl overridden in sub + expect(getVar(staging, 'baseurl')?.value).toBe('https://staging.example.com'); + // nested.name overridden, nested array preserved from base + expect(getVar(staging, 'nested.name')?.value).toBe('bob'); + expect(getVar(staging, 'nested.roles[0]')?.value).toBe('admin'); + + expect(dev.name).toBe('Dev'); + // no sub data => inherits base + expect(getVar(dev, 'baseurl')?.value).toBe('https://api.example.com'); + expect(getVar(dev, 'nested.name')?.value).toBe('alice'); + }); + }); + + describe('buildV4Environments', () => { + it('merges nearest base and sub env data (flattened) into standalone Bruno envs', () => { + const workspaceId = 'wrk_1'; + const resources = [ + { _id: workspaceId, _type: 'workspace', name: 'WS' }, + { + _id: 'env_base', + _type: 'environment', + parentId: workspaceId, + name: 'Base', + data: { + baseurl: 'https://api.example.com', + user: { name: 'alice' }, + arr: [{ id: 1 }] + } + }, + { + _id: 'env_sub', + _type: 'environment', + parentId: 'env_base', + name: 'Sub', + data: { + user: { name: 'bob' } + } + } + ]; + + const envs = buildV4Environments(resources, workspaceId); + expect(envs.length).toBe(2); + + const base = envs.find((e) => e.name === 'Base'); + const sub = envs.find((e) => e.name === 'Sub'); + + expect(getVar(base, 'baseurl')?.value).toBe('https://api.example.com'); + expect(getVar(base, 'user.name')?.value).toBe('alice'); + expect(getVar(base, 'arr[0].id')?.value).toBe('1'); + + // sub should inherit base, override user.name + expect(getVar(sub, 'baseurl')?.value).toBe('https://api.example.com'); + expect(getVar(sub, 'user.name')?.value).toBe('bob'); + expect(getVar(sub, 'arr[0].id')?.value).toBe('1'); + }); + }); +}); diff --git a/packages/bruno-converters/tests/insomnia/insomnia-collection-v5.spec.js b/packages/bruno-converters/tests/insomnia/insomnia-collection-v5.spec.js index 3d620d1db..ddf4261b0 100644 --- a/packages/bruno-converters/tests/insomnia/insomnia-collection-v5.spec.js +++ b/packages/bruno-converters/tests/insomnia/insomnia-collection-v5.spec.js @@ -84,7 +84,27 @@ environments: ` const expectedOutput = { - "environments": [], + environments: [ + { + name: 'Imported Environment', + variables: [ + { + name: 'var1', + value: 'value1', + type: 'text', + enabled: true, + secret: false + }, + { + name: 'var2', + value: 'value2', + type: 'text', + enabled: true, + secret: false + } + ] + } + ], "items": [ { "items": [ diff --git a/packages/bruno-converters/tests/insomnia/insomnia-collection.spec.js b/packages/bruno-converters/tests/insomnia/insomnia-collection.spec.js index 03df7e44f..2b1ff40ca 100644 --- a/packages/bruno-converters/tests/insomnia/insomnia-collection.spec.js +++ b/packages/bruno-converters/tests/insomnia/insomnia-collection.spec.js @@ -65,7 +65,27 @@ const insomniaCollection = { }; const expectedOutput = { - "environments": [], + environments: [ + { + name: 'Environment 1', + variables: [ + { + name: 'var1', + value: 'value1', + type: 'text', + enabled: true, + secret: false + }, + { + name: 'var2', + value: 'value2', + type: 'text', + enabled: true, + secret: false + } + ] + } + ], "items": [ { "items": [ diff --git a/packages/bruno-converters/tests/utils/flatten.spec.js b/packages/bruno-converters/tests/utils/flatten.spec.js new file mode 100644 index 000000000..fdc4f94f7 --- /dev/null +++ b/packages/bruno-converters/tests/utils/flatten.spec.js @@ -0,0 +1,55 @@ +import { describe, it, expect } from '@jest/globals'; +import { flattenObject } from '../../src/utils/flatten'; + +describe('flattenObject', () => { + it('returns empty object for empty input object', () => { + expect(flattenObject({})).toEqual({}); + }); + + it('flattens a simple nested object', () => { + const input = { user: { name: 'Tom', info: { id: 1 } } }; + expect(flattenObject(input)).toEqual({ + 'user.name': 'Tom', + 'user.info.id': 1 + }); + }); + + it('flattens arrays using JavaScript-style square bracket notation', () => { + const input = { tags: ['a', 'b'], nums: [1, 2] }; + expect(flattenObject(input)).toEqual({ + 'tags[0]': 'a', + 'tags[1]': 'b', + 'nums[0]': 1, + 'nums[1]': 2 + }); + }); + + it('handles null and primitive leaves correctly', () => { + const input = { a: null, b: true, c: 0, d: 'x' }; + expect(flattenObject(input)).toEqual({ + a: null, + b: true, + c: 0, + d: 'x' + }); + }); + + it('flattens mixed nested objects and arrays', () => { + const input = { + user: { name: 'Tom', roles: ['admin', 'editor'] }, + list: [{ id: 1 }, { id: 2 }] + }; + expect(flattenObject(input)).toEqual({ + 'user.name': 'Tom', + 'user.roles[0]': 'admin', + 'user.roles[1]': 'editor', + 'list[0].id': 1, + 'list[1].id': 2 + }); + }); + + it('ignores empty arrays/objects (no keys produced for empty containers)', () => { + const input = { emptyObj: {}, emptyArr: [] }; + expect(flattenObject(input)).toEqual({}); + }); +}); diff --git a/tests/import/insomnia/fixtures/insomnia-v4-with-envs.json b/tests/import/insomnia/fixtures/insomnia-v4-with-envs.json new file mode 100644 index 000000000..16d694b58 --- /dev/null +++ b/tests/import/insomnia/fixtures/insomnia-v4-with-envs.json @@ -0,0 +1,123 @@ +{ + "_type": "export", + "__export_format": 4, + "__export_date": "2025-01-01T12:00:00.000Z", + "__export_source": "insomnia.desktop.app:v10.3.1", + "resources": [ + { + "_id": "req_fdedb34f7d5541d0aa7a917ce37ec067", + "parentId": "wrk_398c634c4fbc4774bcff39cbff44b31b", + "modified": 1689952276171, + "created": 1689951240510, + "url": "{{baseUrl}}/api/users", + "name": "Get Users", + "description": "Fetch all users from the API", + "method": "GET", + "body": {}, + "parameters": [], + "headers": [ + { + "name": "Accept", + "value": "application/json" + } + ], + "authentication": {}, + "metaSortKey": -1689951414329, + "isPrivate": false, + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, + { + "_id": "wrk_398c634c4fbc4774bcff39cbff44b31b", + "parentId": null, + "modified": 1743678539806, + "created": 1743678539806, + "name": "Test API Collection v4 with Environments", + "description": "Test collection for Insomnia v4 format with environments", + "scope": "collection", + "_type": "workspace" + }, + { + "_id": "env_93781eb62f074459bb67692112b76da0", + "parentId": "wrk_398c634c4fbc4774bcff39cbff44b31b", + "modified": 1743681240772, + "created": 1689951235312, + "name": "Base Environment", + "data": { + "baseUrl": "https://api.example.com", + "authToken": "your_auth_token_here", + "user": { + "name": "admin", + "id": 123, + "roles": ["admin", "user"] + }, + "config": { + "timeout": 30000, + "retries": 3, + "debug": true + } + }, + "dataPropertyOrder": null, + "color": null, + "isPrivate": false, + "metaSortKey": 1689951235312, + "_type": "environment" + }, + { + "_id": "env_staging_123", + "parentId": "env_93781eb62f074459bb67692112b76da0", + "modified": 1743681240772, + "created": 1689951235312, + "name": "Staging", + "data": { + "baseUrl": "https://staging-api.example.com", + "user": { + "name": "staging_admin" + }, + "config": { + "timeout": 60000, + "debug": false + } + }, + "dataPropertyOrder": null, + "color": null, + "isPrivate": false, + "metaSortKey": 1689951235312, + "_type": "environment" + }, + { + "_id": "env_dev_456", + "parentId": "env_93781eb62f074459bb67692112b76da0", + "modified": 1743681240772, + "created": 1689951235312, + "name": "Development", + "data": { + "baseUrl": "https://dev-api.example.com", + "authToken": "dev_token_123", + "newFeature": { + "enabled": true, + "version": 2.099123123 + } + }, + "dataPropertyOrder": null, + "color": null, + "isPrivate": false, + "metaSortKey": 1689951235312, + "_type": "environment" + }, + { + "_id": "jar_09963a0322c24b698ecd2f866ae9a6ab", + "parentId": "wrk_398c634c4fbc4774bcff39cbff44b31b", + "modified": 1689951235313, + "created": 1689951235313, + "name": "Default Jar", + "cookies": [], + "_type": "cookie_jar" + } + ] +} diff --git a/tests/import/insomnia/fixtures/insomnia-v5-with-envs.yaml b/tests/import/insomnia/fixtures/insomnia-v5-with-envs.yaml new file mode 100644 index 000000000..d7c1b4db2 --- /dev/null +++ b/tests/import/insomnia/fixtures/insomnia-v5-with-envs.yaml @@ -0,0 +1,87 @@ +type: collection.insomnia.rest/5.0 +name: Test API Collection v5 with Environments +meta: + id: wrk_7faf891d273e4b7ea82bdbaa641ee17a + created: 1743683067888 + modified: 1743683067888 +collection: + - name: API Tests + meta: + id: fld_ab2a1533f2be48c194883bf07d693292 + created: 1743683080329 + modified: 1743683080329 + sortKey: -1743683080329 + children: + - url: "{{ _.base_url }}/api/users" + name: Get Users + meta: + id: req_0393b8ff4ee1454daddacdda33fd33ea + created: 1743683426423 + modified: 1743683632735 + isPrivate: false + sortKey: -1743683429031 + method: GET + headers: + - name: Authorization + value: Bearer {{ _.auth_token }} + settings: + renderRequestBody: true + encodeUrl: true + followRedirects: global + cookies: + send: true + store: true + rebuildPath: true +cookieJar: + name: Default Jar + meta: + id: jar_25f97142fa796ae37f7f4937c0ebf3a07869d0a8 + created: 1743683067908 + modified: 1743683833282 + cookies: [] +environments: + name: Base Environment + meta: + id: env_25f97142fa796ae37f7f4937c0ebf3a07869d0a8 + created: 1743683067895 + modified: 1743683476058 + isPrivate: false + data: + base_url: https://api.example.com + auth_token: your_auth_token_here + user: + name: admin + id: 123 + roles: + - admin + - user + config: + timeout: 30000 + retries: 3 + debug: true + subEnvironments: + - name: Staging + meta: + id: env_staging_123 + created: 1743683067895 + modified: 1743683476058 + isPrivate: false + data: + base_url: https://staging-api.example.com + user: + name: staging_admin + config: + timeout: 60000 + debug: false + - name: Development + meta: + id: env_dev_456 + created: 1743683067895 + modified: 1743683476058 + isPrivate: false + data: + base_url: https://dev-api.example.com + auth_token: dev_token_123 + new_feature: + enabled: true + version: 2.099123123 diff --git a/tests/import/insomnia/import-insomnia-v4-environments.spec.ts b/tests/import/insomnia/import-insomnia-v4-environments.spec.ts new file mode 100644 index 000000000..fa1e0d8dc --- /dev/null +++ b/tests/import/insomnia/import-insomnia-v4-environments.spec.ts @@ -0,0 +1,185 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; +import { openCollectionAndAcceptSandbox, closeAllCollections } from '../../utils/page/actions'; + +test.describe('Import Insomnia v4 Collection - Environment Import', () => { + test.afterEach(async ({ page }) => { + await closeAllCollections(page); + }); + /** + * Tests Insomnia v4 environment import with nested data flattening and environment merging. + * Verifies that base and sub-environments are imported correctly with JavaScript-style keys + * (e.g., user.name, user.roles[0]) and proper value inheritance/overrides. + * + * Test Structure: + * - Base Environment: Contains nested objects, arrays, and primitive values + * - Staging Environment: Overrides some base values, inherits others + * - Development Environment: Adds new variables while inheriting base values + */ + test('Import Insomnia v4 collection with nested environments and verify flattening', async ({ + page, + createTmpDir + }) => { + const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v4-with-envs.json'); + + await test.step('Import Insomnia v4 collection with environments', async () => { + await page.getByRole('button', { name: 'Import Collection' }).click(); + + const importModal = page.getByTestId('import-collection-modal'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', insomniaFile); + + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + const locationModal = page.getByTestId('import-collection-location-modal'); + await expect(locationModal.getByText('Test API Collection v4 with Environments')).toBeVisible(); + + await page.locator('#collection-location').fill(await createTmpDir('insomnia-v4-env-test')); + await page.getByRole('button', { name: 'Import', exact: true }).click(); + + await expect(page.locator('#sidebar-collection-name').getByText('Test API Collection v4 with Environments')).toBeVisible(); + + await openCollectionAndAcceptSandbox(page, 'Test API Collection v4 with Environments', 'safe'); + }); + + await test.step('Open collection environments panel', async () => { + await page.getByTestId('environment-selector-trigger').click(); + await page.getByTestId('env-tab-collection').click(); + await page.getByRole('button', { name: 'Configure' }).click(); + }); + + await test.step('Verify all environments are present', async () => { + await expect(page + .locator('div') + .filter({ hasText: /^Base Environment$/ }) + .first()).toBeVisible(); + await expect(page + .locator('div') + .filter({ hasText: /^Staging$/ }) + .first()).toBeVisible(); + await expect(page + .locator('div') + .filter({ hasText: /^Development$/ }) + .first()).toBeVisible(); + }); + + await test.step('Test Base Environment - verify flattened keys', async () => { + await page + .locator('div') + .filter({ hasText: /^Base Environment$/ }) + .first() + .click(); + + // **Assertion 1: Basic Variables (Top-level keys)** + // Verifies that simple key-value pairs from the base environment are imported correctly + const v4BaseUrlInput = page.locator('input[value="baseUrl"]'); + const v4AuthTokenInput = page.locator('input[value="authToken"]'); + await expect(v4BaseUrlInput).toBeVisible(); + await expect(v4AuthTokenInput).toBeVisible(); + + // Assert: Top-level string values are preserved exactly as in the source + await expect(page.getByTestId('env-var-row-baseUrl').locator('.CodeMirror-line').first()).toHaveText('https://api.example.com'); + await expect(page.getByTestId('env-var-row-authToken').locator('.CodeMirror-line').first()).toHaveText('your_auth_token_here'); + + // **Assertion 2: Nested Object Flattening** + // Verifies that nested objects are flattened to dot-notation keys (e.g., user.name, user.id) + const v4UserNameInput = page.locator('input[value="user.name"]'); + const v4UserIdInput = page.locator('input[value="user.id"]'); + await expect(v4UserNameInput).toBeVisible(); + await expect(v4UserIdInput).toBeVisible(); + + // Assert: Nested object properties are accessible via dot notation + await expect(page.getByTestId('env-var-row-user.name').locator('.CodeMirror-line').first()).toHaveText('admin'); + // Assert: Numeric values are converted to strings and preserved + await expect(page.getByTestId('env-var-row-user.id').locator('.CodeMirror-line').first()).toHaveText('123'); + + // **Assertion 3: Array Flattening** + // Verifies that arrays are flattened using JavaScript-style square bracket notation (e.g., user.roles[0], user.roles[1]) + const v4UserRoles0Input = page.locator('input[value="user.roles[0]"]'); + const v4UserRoles1Input = page.locator('input[value="user.roles[1]"]'); + await expect(v4UserRoles0Input).toBeVisible(); + await expect(v4UserRoles1Input).toBeVisible(); + + // Assert: Array elements are accessible via JavaScript-style square bracket notation + await expect(page.getByTestId('env-var-row-user.roles[0]').locator('.CodeMirror-line').first()).toHaveText('admin'); + await expect(page.getByTestId('env-var-row-user.roles[1]').locator('.CodeMirror-line').first()).toHaveText('user'); + }); + + await test.step('Test Staging Environment - verify merging with base', async () => { + await page + .locator('div') + .filter({ hasText: /^Staging$/ }) + .first() + .click(); + + // **Assertion 1: Top-level Variable Override** + // Verifies that staging environment overrides base environment values + const v4StagingBaseUrlInput = page.locator('input[value="baseUrl"]'); + await expect(v4StagingBaseUrlInput).toBeVisible(); + // Assert: Staging overrides baseUrl with its own value + await expect(page.getByTestId('env-var-row-baseUrl').locator('.CodeMirror-line').first()).toHaveText('https://staging-api.example.com'); + + // **Assertion 2: Top-level Variable Inheritance** + // Verifies that staging environment inherits base environment values when not overridden + const v4StagingAuthTokenInput = page.locator('input[value="authToken"]'); + await expect(v4StagingAuthTokenInput).toBeVisible(); + // Assert: Staging inherits authToken from base (not overridden in staging) + await expect(page.getByTestId('env-var-row-authToken').locator('.CodeMirror-line').first()).toHaveText('your_auth_token_here'); + + // **Assertion 3: Nested Object Variable Override and Inheritance** + // Verifies that nested object properties can be selectively overridden while inheriting others + const v4StagingUserNameInput = page.locator('input[value="user.name"]'); + const v4StagingUserIdInput = page.locator('input[value="user.id"]'); + const v4StagingUserRoles0Input = page.locator('input[value="user.roles[0]"]'); + await expect(v4StagingUserNameInput).toBeVisible(); + await expect(v4StagingUserIdInput).toBeVisible(); + await expect(v4StagingUserRoles0Input).toBeVisible(); + + // Assert: Staging overrides user.name with its own value + await expect(page.getByTestId('env-var-row-user.name').locator('.CodeMirror-line').first()).toHaveText('staging_admin'); + // Assert: Staging inherits user.id from base (not overridden in staging) + await expect(page.getByTestId('env-var-row-user.id').locator('.CodeMirror-line').first()).toHaveText('123'); + // Assert: Staging inherits user.roles[0] from base (not overridden in staging) + await expect(page.getByTestId('env-var-row-user.roles[0]').locator('.CodeMirror-line').first()).toHaveText('admin'); + }); + + await test.step('Test Development Environment - verify new variables', async () => { + await page + .locator('div') + .filter({ hasText: /^Development$/ }) + .first() + .click(); + + // **Assertion 1: Multiple Top-level Variable Overrides** + // Verifies that development environment can override multiple base environment values + const v4DevBaseUrlInput = page.locator('input[value="baseUrl"]'); + const v4DevAuthTokenInput = page.locator('input[value="authToken"]'); + await expect(v4DevBaseUrlInput).toBeVisible(); + await expect(v4DevAuthTokenInput).toBeVisible(); + + // Assert: Development overrides baseUrl with its own value + await expect(page.getByTestId('env-var-row-baseUrl').locator('.CodeMirror-line').first()).toHaveText('https://dev-api.example.com'); + // Assert: Development overrides authToken with its own value + await expect(page.getByTestId('env-var-row-authToken').locator('.CodeMirror-line').first()).toHaveText('dev_token_123'); + + // **Assertion 2: New Nested Variables Addition** + // Verifies that development environment can add completely new nested variables not present in base + const v4NewFeatureEnabledInput = page.locator('input[value="newFeature.enabled"]'); + const v4NewFeatureVersionInput = page.locator('input[value="newFeature.version"]'); + await expect(v4NewFeatureEnabledInput).toBeVisible(); + await expect(v4NewFeatureVersionInput).toBeVisible(); + + // Assert: New boolean variable is added and converted to string + await expect(page.getByTestId('env-var-row-newFeature.enabled').locator('.CodeMirror-line').first()).toHaveText('true'); + // Assert: New numeric variable is added and converted to string with full precision + await expect(page.getByTestId('env-var-row-newFeature.version').locator('.CodeMirror-line').first()).toHaveText('2.099123123'); + }); + + await test.step('Close environment modal', async () => { + // Close the environment configuration modal to ensure clean state + await page.getByText('×').click(); + }); + }); +}); diff --git a/tests/import/insomnia/import-insomnia-v5-environments.spec.ts b/tests/import/insomnia/import-insomnia-v5-environments.spec.ts new file mode 100644 index 000000000..178621667 --- /dev/null +++ b/tests/import/insomnia/import-insomnia-v5-environments.spec.ts @@ -0,0 +1,212 @@ +import { test, expect } from '../../../playwright'; +import * as path from 'path'; +import { openCollectionAndAcceptSandbox, closeAllCollections } from '../../utils/page/actions'; + +test.describe('Import Insomnia v5 Collection - Environment Import', () => { + test.afterEach(async ({ page }) => { + await closeAllCollections(page); + }); + /** + * Tests Insomnia v5 environment import with nested data flattening and environment merging. + * Verifies that base and sub-environments are imported correctly with JavaScript-style keys + * (e.g., user.name, user.roles[0]) and proper value inheritance/overrides. + * + * Test Structure: + * - Base Environment: Contains nested objects, arrays, and primitive values + * - Staging Environment: Overrides some base values, inherits others + * - Development Environment: Adds new variables while inheriting base values + */ + test('Import Insomnia v5 collection with nested environments and verify flattening', async ({ + page, + createTmpDir + }) => { + const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v5-with-envs.yaml'); + + await test.step('Import Insomnia v5 collection with environments', async () => { + await page.getByRole('button', { name: 'Import Collection' }).click(); + + const importModal = page.getByTestId('import-collection-modal'); + await importModal.waitFor({ state: 'visible' }); + await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection'); + + await page.setInputFiles('input[type="file"]', insomniaFile); + + await page.locator('#import-collection-loader').waitFor({ state: 'hidden' }); + + const locationModal = page.getByTestId('import-collection-location-modal'); + await expect(locationModal.getByText('Test API Collection v5 with Environments')).toBeVisible(); + + await page.locator('#collection-location').fill(await createTmpDir('insomnia-v5-env-test')); + await page.getByRole('button', { name: 'Import', exact: true }).click(); + + await expect(page.getByText('Test API Collection v5 with Environments')).toBeVisible(); + + await openCollectionAndAcceptSandbox(page, 'Test API Collection v5 with Environments', 'safe'); + }); + + await test.step('Open collection environments panel', async () => { + await page.getByTestId('environment-selector-trigger').click(); + await page.getByTestId('env-tab-collection').click(); + await page.getByRole('button', { name: 'Configure' }).click(); + }); + + await test.step('Verify all environments are present', async () => { + await expect(page + .locator('div') + .filter({ hasText: /^Base Environment$/ }) + .first()).toBeVisible(); + await expect(page + .locator('div') + .filter({ hasText: /^Staging$/ }) + .first()).toBeVisible(); + await expect(page + .locator('div') + .filter({ hasText: /^Development$/ }) + .first()).toBeVisible(); + }); + + await test.step('Test Base Environment - verify flattened keys', async () => { + await page + .locator('div') + .filter({ hasText: /^Base Environment$/ }) + .first() + .click(); + + // **Assertion 1: Basic Variables (Top-level keys)** + // Verifies that simple key-value pairs from the base environment are imported correctly + const baseUrlInput = page.locator('input[value="base_url"]'); + const authTokenInput = page.locator('input[value="auth_token"]'); + await expect(baseUrlInput).toBeVisible(); + await expect(authTokenInput).toBeVisible(); + + // Assert: Top-level string values are preserved exactly as in the source + await expect(page.getByTestId('env-var-row-base_url').locator('.CodeMirror-line').first()).toHaveText('https://api.example.com'); + await expect(page.getByTestId('env-var-row-auth_token').locator('.CodeMirror-line').first()).toHaveText('your_auth_token_here'); + + // **Assertion 2: Nested Object Flattening** + // Verifies that nested objects are flattened to dot-notation keys (e.g., user.name, user.id) + const userNameInput = page.locator('input[value="user.name"]'); + const userIdInput = page.locator('input[value="user.id"]'); + await expect(userNameInput).toBeVisible(); + await expect(userIdInput).toBeVisible(); + + // Assert: Nested object properties are accessible via dot notation + await expect(page.getByTestId('env-var-row-user.name').locator('.CodeMirror-line').first()).toHaveText('admin'); + // Assert: Numeric values are converted to strings and preserved + await expect(page.getByTestId('env-var-row-user.id').locator('.CodeMirror-line').first()).toHaveText('123'); + + // **Assertion 3: Array Flattening** + // Verifies that arrays are flattened using JavaScript-style square bracket notation (e.g., user.roles[0], user.roles[1]) + const userRoles0Input = page.locator('input[value="user.roles[0]"]'); + const userRoles1Input = page.locator('input[value="user.roles[1]"]'); + await expect(userRoles0Input).toBeVisible(); + await expect(userRoles1Input).toBeVisible(); + + // Assert: Array elements are accessible via JavaScript-style square bracket notation + await expect(page.getByTestId('env-var-row-user.roles[0]').locator('.CodeMirror-line').first()).toHaveText('admin'); + await expect(page.getByTestId('env-var-row-user.roles[1]').locator('.CodeMirror-line').first()).toHaveText('user'); + + // **Assertion 4: Deeply Nested Config Objects** + // Verifies that deeply nested objects are properly flattened (e.g., config.timeout, config.debug) + const configTimeoutInput = page.locator('input[value="config.timeout"]'); + const configDebugInput = page.locator('input[value="config.debug"]'); + await expect(configTimeoutInput).toBeVisible(); + await expect(configDebugInput).toBeVisible(); + + // Assert: Numeric values in nested objects are converted to strings + await expect(page.getByTestId('env-var-row-config.timeout').locator('.CodeMirror-line').first()).toHaveText('30000'); + // Assert: Boolean values in nested objects are converted to strings + await expect(page.getByTestId('env-var-row-config.debug').locator('.CodeMirror-line').first()).toHaveText('true'); + }); + + await test.step('Test Staging Environment - verify merging and overrides', async () => { + await page + .locator('div') + .filter({ hasText: /^Staging$/ }) + .first() + .click(); + + // **Assertion 1: Top-level Variable Override** + // Verifies that staging environment overrides base environment values + const stagingBaseUrlInput = page.locator('input[value="base_url"]'); + await expect(stagingBaseUrlInput).toBeVisible(); + // Assert: Staging overrides base_url with its own value + await expect(page.getByTestId('env-var-row-base_url').locator('.CodeMirror-line').first()).toHaveText('https://staging-api.example.com'); + + // **Assertion 2: Top-level Variable Inheritance** + // Verifies that staging environment inherits base environment values when not overridden + const stagingAuthTokenInput = page.locator('input[value="auth_token"]'); + await expect(stagingAuthTokenInput).toBeVisible(); + // Assert: Staging inherits auth_token from base (not overridden in staging) + await expect(page.getByTestId('env-var-row-auth_token').locator('.CodeMirror-line').first()).toHaveText('your_auth_token_here'); + + // **Assertion 3: Nested Object Variable Override and Inheritance** + // Verifies that nested object properties can be selectively overridden while inheriting others + const stagingUserNameInput = page.locator('input[value="user.name"]'); + const stagingUserIdInput = page.locator('input[value="user.id"]'); + await expect(stagingUserNameInput).toBeVisible(); + await expect(stagingUserIdInput).toBeVisible(); + + // Assert: Staging overrides user.name with its own value + await expect(page.getByTestId('env-var-row-user.name').locator('.CodeMirror-line').first()).toHaveText('staging_admin'); + // Assert: Staging inherits user.id from base (not overridden in staging) + await expect(page.getByTestId('env-var-row-user.id').locator('.CodeMirror-line').first()).toHaveText('123'); + + // **Assertion 4: Deeply Nested Config Override** + // Verifies that deeply nested object properties can be overridden + const stagingConfigTimeoutInput = page.locator('input[value="config.timeout"]'); + const stagingConfigDebugInput = page.locator('input[value="config.debug"]'); + await expect(stagingConfigTimeoutInput).toBeVisible(); + await expect(stagingConfigDebugInput).toBeVisible(); + + // Assert: Staging overrides config.timeout with its own value + await expect(page.getByTestId('env-var-row-config.timeout').locator('.CodeMirror-line').first()).toHaveText('60000'); + // Assert: Staging overrides config.debug with its own value + await expect(page.getByTestId('env-var-row-config.debug').locator('.CodeMirror-line').first()).toHaveText('false'); + }); + + await test.step('Test Development Environment - verify new variables', async () => { + await page + .locator('div') + .filter({ hasText: /^Development$/ }) + .first() + .click(); + + // **Assertion 1: Multiple Top-level Variable Overrides** + // Verifies that development environment can override multiple base environment values + const devBaseUrlInput = page.locator('input[value="base_url"]'); + const devAuthTokenInput = page.locator('input[value="auth_token"]'); + await expect(devBaseUrlInput).toBeVisible(); + await expect(devAuthTokenInput).toBeVisible(); + + // Assert: Development overrides base_url with its own value + await expect(page.getByTestId('env-var-row-base_url').locator('.CodeMirror-line').first()).toHaveText('https://dev-api.example.com'); + // Assert: Development overrides auth_token with its own value + await expect(page.getByTestId('env-var-row-auth_token').locator('.CodeMirror-line').first()).toHaveText('dev_token_123'); + + // **Assertion 2: New Nested Variables Addition** + // Verifies that development environment can add completely new nested variables not present in base + const newFeatureEnabledInput = page.locator('input[value="new_feature.enabled"]'); + const newFeatureVersionInput = page.locator('input[value="new_feature.version"]'); + await expect(newFeatureEnabledInput).toBeVisible(); + await expect(newFeatureVersionInput).toBeVisible(); + + // Assert: New boolean variable is added and converted to string + await expect(page.getByTestId('env-var-row-new_feature.enabled').locator('.CodeMirror-line').first()).toHaveText('true'); + // Assert: New numeric variable is added and converted to string with full precision + await expect(page.getByTestId('env-var-row-new_feature.version').locator('.CodeMirror-line').first()).toHaveText('2.099123123'); + + // **Assertion 3: Base Variable Inheritance** + // Verifies that development environment still inherits base variables that are not overridden + const devUserRoles0Input = page.locator('input[value="user.roles[0]"]'); + await expect(devUserRoles0Input).toBeVisible(); + // Assert: Development inherits user.roles[0] from base (not overridden in development) + await expect(page.getByTestId('env-var-row-user.roles[0]').locator('.CodeMirror-line').first()).toHaveText('admin'); + }); + + await test.step('Close environment modal', async () => { + // Close the environment configuration modal to ensure clean state + await page.getByText('×').click(); + }); + }); +});