From 91f8e4a82dd1d6f0ab94c8b51c6c02cf0137f07e Mon Sep 17 00:00:00 2001 From: Maciej Krajowski-Kukiel Date: Wed, 14 Jan 2026 14:52:04 +0100 Subject: [PATCH 1/4] fix ci after adding file path to unknown property error --- test/deploy.test.js | 2 +- .../deploy/correct_with_assets/app/assets/bar.js | 1 + .../deploy/modules_update/app/pos-modules.json | 2 +- .../deploy/modules_update/app/pos-modules.lock.json | 2 +- test/sync.test.js | 10 ++++++++++ 5 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/deploy/correct_with_assets/app/assets/bar.js diff --git a/test/deploy.test.js b/test/deploy.test.js index 73ecdd38..5dabe827 100644 --- a/test/deploy.test.js +++ b/test/deploy.test.js @@ -105,7 +105,7 @@ describe('Server errors', () => { test('Error in form', async () => { const { stderr } = await run('incorrect_form'); expect(stderr).toMatch( - 'Unknown properties: hello. Available properties are: api_call_notifications, async_callback_actions, authorization_policies, body, callback_actions, default_payload, email_notifications, fields, flash_alert, flash_notice, live_reindex, metadata, name, redirect_to, request_allowed, resource, resource_owner, response_headers, return_to, sms_notifications, spam_protection.' + 'Unknown properties in `form_configurations/hello.liquid`: hello. Available properties are: api_call_notifications, async_callback_actions, authorization_policies, body, callback_actions, default_payload, email_notifications, fields, flash_alert, flash_notice, live_reindex, metadata, name, redirect_to, request_allowed, resource, resource_owner, response_headers, return_to, sms_notifications, spam_protection.' ); }); diff --git a/test/fixtures/deploy/correct_with_assets/app/assets/bar.js b/test/fixtures/deploy/correct_with_assets/app/assets/bar.js new file mode 100644 index 00000000..a693db7f --- /dev/null +++ b/test/fixtures/deploy/correct_with_assets/app/assets/bar.js @@ -0,0 +1 @@ +// Test asset file diff --git a/test/fixtures/deploy/modules_update/app/pos-modules.json b/test/fixtures/deploy/modules_update/app/pos-modules.json index 970703eb..55aee05b 100644 --- a/test/fixtures/deploy/modules_update/app/pos-modules.json +++ b/test/fixtures/deploy/modules_update/app/pos-modules.json @@ -1,5 +1,5 @@ { "modules": { - "core": "1.5.5" + "core": "2.0.7" } } \ No newline at end of file diff --git a/test/fixtures/deploy/modules_update/app/pos-modules.lock.json b/test/fixtures/deploy/modules_update/app/pos-modules.lock.json index 970703eb..55aee05b 100644 --- a/test/fixtures/deploy/modules_update/app/pos-modules.lock.json +++ b/test/fixtures/deploy/modules_update/app/pos-modules.lock.json @@ -1,5 +1,5 @@ { "modules": { - "core": "1.5.5" + "core": "2.0.7" } } \ No newline at end of file diff --git a/test/sync.test.js b/test/sync.test.js index d90f5738..4edb9c63 100644 --- a/test/sync.test.js +++ b/test/sync.test.js @@ -3,6 +3,7 @@ const exec = require('./utils/exec'); const cliPath = require('./utils/cliPath'); const path = require('path'); +const fs = require('fs'); const stepTimeout = 3500; @@ -28,6 +29,15 @@ const kill = p => { jest.retryTimes(2); +// Store original content to restore after tests +const barJsPath = path.join(cwd('correct_with_assets'), 'app/assets/bar.js'); +const originalBarJsContent = fs.readFileSync(barJsPath, 'utf8'); + +afterAll(() => { + // Restore bar.js to original content after all tests + fs.writeFileSync(barJsPath, originalBarJsContent); +}); + describe('Happy path', () => { test('sync assets', async () => { From 793adaf08e5620e446478671897d4554d63d97b0 Mon Sep 17 00:00:00 2001 From: Maciej Krajowski-Kukiel Date: Wed, 14 Jan 2026 16:00:52 +0100 Subject: [PATCH 2/4] add pos-cli exec liquid command --- bin/pos-cli-exec-liquid.js | 33 ++++++++++ bin/pos-cli-exec.js | 8 +++ bin/pos-cli.js | 1 + test/exec-liquid.test.js | 120 +++++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+) create mode 100644 bin/pos-cli-exec-liquid.js create mode 100644 bin/pos-cli-exec.js create mode 100644 test/exec-liquid.test.js diff --git a/bin/pos-cli-exec-liquid.js b/bin/pos-cli-exec-liquid.js new file mode 100644 index 00000000..ae7fb3ca --- /dev/null +++ b/bin/pos-cli-exec-liquid.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node + +const { program } = require('commander'); +const Gateway = require('../lib/proxy'); +const fetchAuthData = require('../lib/settings').fetchSettings; +const logger = require('../lib/logger'); + +program + .name('pos-cli exec liquid') + .argument('', 'name of environment. Example: staging') + .argument('', 'liquid code to execute as string') + .action(async (environment, code) => { + const authData = fetchAuthData(environment, program); + const gateway = new Gateway(authData); + + try { + const response = await gateway.liquid({ content: code }); + + if (response.error) { + logger.Error(`Liquid execution error: ${response.error}`); + process.exit(1); + } + + if (response.result) { + logger.Print(response.result); + } + } catch (error) { + logger.Error(`Failed to execute liquid: ${error.message}`); + process.exit(1); + } + }); + +program.parse(process.argv); \ No newline at end of file diff --git a/bin/pos-cli-exec.js b/bin/pos-cli-exec.js new file mode 100644 index 00000000..ac16ba86 --- /dev/null +++ b/bin/pos-cli-exec.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +const { program } = require('commander'); + +program + .name('pos-cli exec') + .command('liquid ', 'execute liquid code on instance') + .parse(process.argv); \ No newline at end of file diff --git a/bin/pos-cli.js b/bin/pos-cli.js index 78d4d467..1463d9b9 100755 --- a/bin/pos-cli.js +++ b/bin/pos-cli.js @@ -24,6 +24,7 @@ program .command('data', 'export, import or clean data on instance') .command('deploy ', 'deploy code to environment').alias('d') .command('env', 'manage environments') + .command('exec', 'execute code on instance') .command('gui', 'gui for content editor, graphql, logs') .command('generate', 'generates files') .command('init', 'initialize directory structure') diff --git a/test/exec-liquid.test.js b/test/exec-liquid.test.js new file mode 100644 index 00000000..09144c39 --- /dev/null +++ b/test/exec-liquid.test.js @@ -0,0 +1,120 @@ +jest.mock('../lib/apiRequest', () => ({ + apiRequest: jest.fn() +})); + +const Gateway = require('../lib/proxy'); + +describe('Gateway liquid method', () => { + const { apiRequest } = require('../lib/apiRequest'); + + test('calls apiRequest with correct parameters', async () => { + apiRequest.mockResolvedValue({ result: 'HELLO WORLD', error: null }); + + const gateway = new Gateway({ url: 'http://example.com', token: '1234', email: 'test@example.com' }); + const result = await gateway.liquid({ content: "{{ 'hello world' | upcase }}" }); + + expect(apiRequest).toHaveBeenCalledWith({ + method: 'POST', + uri: 'http://example.com/api/app_builder/liquid_exec', + json: { content: "{{ 'hello world' | upcase }}" }, + forever: true, + request: expect.any(Function) + }); + expect(result).toEqual({ result: 'HELLO WORLD', error: null }); + }); + + test('handles liquid execution error', async () => { + apiRequest.mockResolvedValue({ result: null, error: 'Liquid syntax error' }); + + const gateway = new Gateway({ url: 'http://example.com', token: '1234', email: 'test@example.com' }); + const result = await gateway.liquid({ content: "{{ 'hello world' | invalid_filter }}" }); + + expect(result).toEqual({ result: null, error: 'Liquid syntax error' }); + }); +}); + +describe('exec liquid CLI', () => { + const exec = require('./utils/exec'); + const cliPath = require('./utils/cliPath'); + + const env = Object.assign(process.env, { + CI: true, + MPKIT_URL: 'http://example.com', + MPKIT_TOKEN: '1234', + MPKIT_EMAIL: 'foo@example.com' + }); + + test('requires code argument', async () => { + const { code, stderr } = await exec(`${cliPath} exec liquid staging`, { env }); + + expect(code).toEqual(1); + expect(stderr).toMatch("error: missing required argument 'code'"); + }); +}); + +// Integration test - requires real platformOS instance +describe('exec liquid integration', () => { + const exec = require('./utils/exec'); + const cliPath = require('./utils/cliPath'); + + // Only run if real credentials are available + const hasRealCredentials = process.env.MPKIT_URL && + process.env.MPKIT_TOKEN && + !process.env.MPKIT_URL.includes('example.com'); + + (hasRealCredentials ? test : test.skip)('executes liquid code on real instance', async () => { + const { stdout, stderr, code } = await exec(`${cliPath} exec liquid dev "{{ 'hello' | upcase }}"`, { + env: process.env, + timeout: 30000 + }); + + expect(code).toEqual(0); + expect(stdout).toMatch('HELLO'); + expect(stderr).toBe(''); + }, 30000); + + (hasRealCredentials ? test : test.skip)('handles liquid syntax error on real instance', async () => { + const { stdout, stderr, code } = await exec(`${cliPath} exec liquid dev "{{ 'hello' | invalid_filter }}"`, { + env: process.env, + timeout: 30000 + }); + + expect(code).toEqual(1); + expect(stderr).toMatch('Liquid execution error'); + }, 30000); + + (hasRealCredentials ? test : test.skip)('executes {{ \'now\' | to_time }} and returns current time', async () => { + const beforeTime = new Date(); + const { stdout, stderr, code } = await exec(`${cliPath} exec liquid dev "{{ 'now' | to_time }}"`, { + env: process.env, + timeout: 30000 + }); + const afterTime = new Date(); + + expect(code).toEqual(0); + expect(stderr).toBe(''); + + // Parse the returned time - liquid to_time returns ISO format like "2023-01-01 12:00:00 +0000" + const returnedTimeStr = stdout.trim(); + const returnedTime = new Date(returnedTimeStr); + + // Check that the returned time is within 1 second of the current time + const timeDiff = Math.abs(returnedTime.getTime() - beforeTime.getTime()); + expect(timeDiff).toBeLessThanOrEqual(1000); // 1 second in milliseconds + + // Also check it's not in the future beyond our test window + const futureDiff = afterTime.getTime() - returnedTime.getTime(); + expect(futureDiff).toBeGreaterThanOrEqual(0); + expect(futureDiff).toBeLessThanOrEqual(1000); + }, 30000); + + (hasRealCredentials ? test : test.skip)('handles unknown tag error', async () => { + const { stdout, stderr, code } = await exec(`${cliPath} exec liquid dev "{% hello %}"`, { + env: process.env, + timeout: 30000 + }); + + expect(code).toEqual(1); + expect(stderr).toMatch('Liquid execution error: Liquid syntax error: Unknown tag \'hello\''); + }, 30000); +}); From fbf2569b2d72d9dca32997f6df0112998998db79 Mon Sep 17 00:00:00 2001 From: Maciej Krajowski-Kukiel Date: Wed, 14 Jan 2026 16:12:24 +0100 Subject: [PATCH 3/4] add pos-cli exec graphql command --- bin/pos-cli-exec-graphql.js | 33 +++++++++++ bin/pos-cli-exec.js | 1 + test/exec-graphql.test.js | 109 ++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 bin/pos-cli-exec-graphql.js create mode 100644 test/exec-graphql.test.js diff --git a/bin/pos-cli-exec-graphql.js b/bin/pos-cli-exec-graphql.js new file mode 100644 index 00000000..5f0e548e --- /dev/null +++ b/bin/pos-cli-exec-graphql.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node + +const { program } = require('commander'); +const Gateway = require('../lib/proxy'); +const fetchAuthData = require('../lib/settings').fetchSettings; +const logger = require('../lib/logger'); + +program + .name('pos-cli exec graphql') + .argument('', 'name of environment. Example: staging') + .argument('', 'graphql query to execute as string') + .action(async (environment, graphql) => { + const authData = fetchAuthData(environment, program); + const gateway = new Gateway(authData); + + try { + const response = await gateway.graph({ query: graphql }); + + if (response.errors) { + logger.Error(`GraphQL execution error: ${JSON.stringify(response.errors, null, 2)}`); + process.exit(1); + } + + if (response.data) { + logger.Print(JSON.stringify(response, null, 2)); + } + } catch (error) { + logger.Error(`Failed to execute graphql: ${error.message}`); + process.exit(1); + } + }); + +program.parse(process.argv); \ No newline at end of file diff --git a/bin/pos-cli-exec.js b/bin/pos-cli-exec.js index ac16ba86..e4df94fb 100644 --- a/bin/pos-cli-exec.js +++ b/bin/pos-cli-exec.js @@ -5,4 +5,5 @@ const { program } = require('commander'); program .name('pos-cli exec') .command('liquid ', 'execute liquid code on instance') + .command('graphql ', 'execute graphql query on instance') .parse(process.argv); \ No newline at end of file diff --git a/test/exec-graphql.test.js b/test/exec-graphql.test.js new file mode 100644 index 00000000..ac922abb --- /dev/null +++ b/test/exec-graphql.test.js @@ -0,0 +1,109 @@ +jest.mock('../lib/apiRequest', () => ({ + apiRequest: jest.fn() +})); + +const Gateway = require('../lib/proxy'); + +describe('Gateway graph method', () => { + const { apiRequest } = require('../lib/apiRequest'); + + test('calls apiRequest with correct parameters', async () => { + const mockResponse = { + "data": { + "records": { + "results": [] + } + } + }; + apiRequest.mockResolvedValue(mockResponse); + + const gateway = new Gateway({ url: 'http://example.com', token: '1234', email: 'test@example.com' }); + const query = '{ records(per_page: 20) { results { id } } }'; + const result = await gateway.graph({ query }); + + expect(apiRequest).toHaveBeenCalledWith({ + method: 'POST', + uri: 'http://example.com/api/graph', + json: { query }, + forever: true, + request: expect.any(Function) + }); + expect(result).toEqual(mockResponse); + }); + + test('handles graphql execution error', async () => { + const mockErrorResponse = { + errors: [ + { + message: 'Syntax Error: Expected Name, found ', + locations: [{ line: 1, column: 40 }] + } + ] + }; + apiRequest.mockResolvedValue(mockErrorResponse); + + const gateway = new Gateway({ url: 'http://example.com', token: '1234', email: 'test@example.com' }); + const query = '{ records(per_page: 20) { results { id } '; // Missing closing brace + const result = await gateway.graph({ query }); + + expect(result).toEqual(mockErrorResponse); + }); +}); + +describe('exec graphql CLI', () => { + const exec = require('./utils/exec'); + const cliPath = require('./utils/cliPath'); + + const env = Object.assign(process.env, { + CI: true, + MPKIT_URL: 'http://example.com', + MPKIT_TOKEN: '1234', + MPKIT_EMAIL: 'foo@example.com' + }); + + test('requires graphql argument', async () => { + const { code, stderr } = await exec(`${cliPath} exec graphql staging`, { env }); + + expect(code).toEqual(1); + expect(stderr).toMatch("error: missing required argument 'graphql'"); + }); +}); + +// Integration test - requires real platformOS instance +describe('exec graphql integration', () => { + const exec = require('./utils/exec'); + const cliPath = require('./utils/cliPath'); + + // Only run if real credentials are available + const hasRealCredentials = process.env.MPKIT_URL && + process.env.MPKIT_TOKEN && + !process.env.MPKIT_URL.includes('example.com'); + + (hasRealCredentials ? test : test.skip)('executes graphql query on real instance', async () => { + const query = '{ records(per_page: 20) { results { id } } }'; + const { stdout, stderr, code } = await exec(`${cliPath} exec graphql dev "${query}"`, { + env: process.env, + timeout: 30000 + }); + + expect(code).toEqual(0); + expect(stderr).toBe(''); + + // Parse JSON response + const response = JSON.parse(stdout); + expect(response).toHaveProperty('data'); + expect(response.data).toHaveProperty('records'); + expect(Array.isArray(response.data.records.results)).toBe(true); + }, 30000); + + (hasRealCredentials ? test : test.skip)('handles graphql syntax error on real instance', async () => { + const invalidQuery = '{ records(per_page: 20) { results { id } '; // Missing closing brace + const { stdout, stderr, code } = await exec(`${cliPath} exec graphql dev "${invalidQuery}"`, { + env: process.env, + timeout: 30000 + }); + + expect(code).toEqual(1); + expect(stderr).toMatch('GraphQL execution error'); + }, 30000); +}); \ No newline at end of file From ff85cc6f96eb9474ba00858f89e4804ede31e657 Mon Sep 17 00:00:00 2001 From: Maciej Krajowski-Kukiel Date: Wed, 14 Jan 2026 16:19:26 +0100 Subject: [PATCH 4/4] extra confirmation for production --- bin/pos-cli-exec-graphql.js | 28 ++++++++++++++++++++++++++++ bin/pos-cli-exec-liquid.js | 28 ++++++++++++++++++++++++++++ test/exec-graphql.test.js | 22 ++++++++++++++++++++++ test/exec-liquid.test.js | 22 ++++++++++++++++++++++ 4 files changed, 100 insertions(+) diff --git a/bin/pos-cli-exec-graphql.js b/bin/pos-cli-exec-graphql.js index 5f0e548e..f0d84fc6 100644 --- a/bin/pos-cli-exec-graphql.js +++ b/bin/pos-cli-exec-graphql.js @@ -1,10 +1,30 @@ #!/usr/bin/env node const { program } = require('commander'); +const prompts = require('prompts'); const Gateway = require('../lib/proxy'); const fetchAuthData = require('../lib/settings').fetchSettings; const logger = require('../lib/logger'); +const isProductionEnvironment = (environment) => { + return environment && (environment.toLowerCase().includes('prod') || environment.toLowerCase().includes('production')); +}; + +const confirmProductionExecution = async (environment) => { + logger.Warn(`WARNING: You are executing GraphQL on a production environment: ${environment}`); + logger.Warn('This could potentially modify production data or cause unintended side effects.'); + logger.Warn(''); + + const response = await prompts({ + type: 'confirm', + name: 'confirmed', + message: `Are you sure you want to continue executing on ${environment}?`, + initial: false + }); + + return response.confirmed; +}; + program .name('pos-cli exec graphql') .argument('', 'name of environment. Example: staging') @@ -13,6 +33,14 @@ program const authData = fetchAuthData(environment, program); const gateway = new Gateway(authData); + if (isProductionEnvironment(environment)) { + const confirmed = await confirmProductionExecution(environment); + if (!confirmed) { + logger.Info('Execution cancelled.'); + process.exit(0); + } + } + try { const response = await gateway.graph({ query: graphql }); diff --git a/bin/pos-cli-exec-liquid.js b/bin/pos-cli-exec-liquid.js index ae7fb3ca..0947277f 100644 --- a/bin/pos-cli-exec-liquid.js +++ b/bin/pos-cli-exec-liquid.js @@ -1,10 +1,30 @@ #!/usr/bin/env node const { program } = require('commander'); +const prompts = require('prompts'); const Gateway = require('../lib/proxy'); const fetchAuthData = require('../lib/settings').fetchSettings; const logger = require('../lib/logger'); +const isProductionEnvironment = (environment) => { + return environment && (environment.toLowerCase().includes('prod') || environment.toLowerCase().includes('production')); +}; + +const confirmProductionExecution = async (environment) => { + logger.Warn(`WARNING: You are executing liquid code on a production environment: ${environment}`); + logger.Warn('This could potentially modify production data or cause unintended side effects.'); + logger.Warn(''); + + const response = await prompts({ + type: 'confirm', + name: 'confirmed', + message: `Are you sure you want to continue executing on ${environment}?`, + initial: false + }); + + return response.confirmed; +}; + program .name('pos-cli exec liquid') .argument('', 'name of environment. Example: staging') @@ -13,6 +33,14 @@ program const authData = fetchAuthData(environment, program); const gateway = new Gateway(authData); + if (isProductionEnvironment(environment)) { + const confirmed = await confirmProductionExecution(environment); + if (!confirmed) { + logger.Info('Execution cancelled.'); + process.exit(0); + } + } + try { const response = await gateway.liquid({ content: code }); diff --git a/test/exec-graphql.test.js b/test/exec-graphql.test.js index ac922abb..2763a5a9 100644 --- a/test/exec-graphql.test.js +++ b/test/exec-graphql.test.js @@ -67,6 +67,28 @@ describe('exec graphql CLI', () => { expect(code).toEqual(1); expect(stderr).toMatch("error: missing required argument 'graphql'"); }); + + test('cancels execution on production environment when user says no', async () => { + const { code, stdout, stderr } = await exec(`echo "n" | ${cliPath} exec graphql production "{ records { results { id } } }"`, { env }); + + expect(code).toEqual(0); + expect(stdout).toMatch('Execution cancelled.'); + }); + + test('proceeds with execution on production environment when user confirms', async () => { + const { code, stdout, stderr } = await exec(`echo "y" | ${cliPath} exec graphql production "{ records { results { id } } }"`, { env }); + + // This will fail because the mock API isn't set up, but we want to check it doesn't cancel + expect(stdout).not.toMatch('Execution cancelled.'); + expect(stderr).not.toMatch('Execution cancelled.'); + }); + + test('does not prompt for non-production environments', async () => { + const { code, stdout, stderr } = await exec(`${cliPath} exec graphql staging "{ records { results { id } } }"`, { env }); + + expect(stdout).not.toMatch('WARNING: You are executing GraphQL on a production environment'); + expect(stdout).not.toMatch('Execution cancelled.'); + }); }); // Integration test - requires real platformOS instance diff --git a/test/exec-liquid.test.js b/test/exec-liquid.test.js index 09144c39..a8f64449 100644 --- a/test/exec-liquid.test.js +++ b/test/exec-liquid.test.js @@ -50,6 +50,28 @@ describe('exec liquid CLI', () => { expect(code).toEqual(1); expect(stderr).toMatch("error: missing required argument 'code'"); }); + + test('cancels execution on production environment when user says no', async () => { + const { code, stdout, stderr } = await exec(`echo "n" | ${cliPath} exec liquid production "{{ 'hello' | upcase }}"`, { env }); + + expect(code).toEqual(0); + expect(stdout).toMatch('Execution cancelled.'); + }); + + test('proceeds with execution on production environment when user confirms', async () => { + const { code, stdout, stderr } = await exec(`echo "y" | ${cliPath} exec liquid production "{{ 'hello' | upcase }}"`, { env }); + + // This will fail because the mock API isn't set up, but we want to check it doesn't cancel + expect(stdout).not.toMatch('Execution cancelled.'); + expect(stderr).not.toMatch('Execution cancelled.'); + }); + + test('does not prompt for non-production environments', async () => { + const { code, stdout, stderr } = await exec(`${cliPath} exec liquid staging "{{ 'hello' | upcase }}"`, { env }); + + expect(stdout).not.toMatch('WARNING: You are executing liquid code on a production environment'); + expect(stdout).not.toMatch('Execution cancelled.'); + }); }); // Integration test - requires real platformOS instance