Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
479ffae
feat(rechallenge): add types and error classes
rinatkhaziev Jun 4, 2026
586b7af
feat(rechallenge): add keychain-backed per-scope elevated-token cache
rinatkhaziev Jun 4, 2026
cd414f0
feat(rechallenge): add REST client for Parker v2 session endpoints
rinatkhaziev Jun 4, 2026
5b0fab9
fix(rechallenge): satisfy tsc check-types in test files
rinatkhaziev Jun 4, 2026
49e5a49
feat(rechallenge): orchestrate session create, poll, exchange
rinatkhaziev Jun 4, 2026
6b938df
feat(rechallenge): add Apollo link that intercepts elevated-permissio…
rinatkhaziev Jun 4, 2026
96161a9
feat(api): insert rechallenge link into Apollo chain
rinatkhaziev Jun 4, 2026
da63adf
feat(defensive-mode): add GraphQL helpers for status and config mutat…
rinatkhaziev Jun 4, 2026
3566bbf
feat(cli): add vip defensive-mode parent command
rinatkhaziev Jun 4, 2026
b6a676d
feat(cli): add vip defensive-mode enable subcommand
rinatkhaziev Jun 4, 2026
b800278
feat(cli): add vip defensive-mode disable subcommand
rinatkhaziev Jun 4, 2026
a90d285
feat(cli): add vip defensive-mode configure subcommand
rinatkhaziev Jun 4, 2026
a697290
feat(cli): register vip defensive-mode top-level command
rinatkhaziev Jun 4, 2026
9210d1f
feat(logout): clear elevated-token cache on logout
rinatkhaziev Jun 4, 2026
d489e55
fix(api): lint cleanup for lazy rechallenge link import
rinatkhaziev Jun 4, 2026
e94f81a
fix: address final-review findings (diff in configure, non-interactiv…
rinatkhaziev Jun 4, 2026
4fa6269
fix: address PR review feedback (data guards, version constant, paylo…
rinatkhaziev Jun 4, 2026
4879454
fix: address PR review feedback (output formatting, login hardening, …
rinatkhaziev Jun 10, 2026
df88602
chore(lint): scope no-await-in-loop disable to rechallenge polling loop
rinatkhaziev Jun 10, 2026
c4e5e96
fix(lint): stop importing Response type from node-fetch in rechalleng…
rinatkhaziev Jun 10, 2026
9ce0cd9
fix: address sjinks PR review feedback (rechallenge + defensive-mode)
rinatkhaziev Jun 22, 2026
920ab77
test: cover rechallenge poll-interval floor (NaN / tight-loop guard)
rinatkhaziev Jun 22, 2026
62ae49e
fix(test): make rechallenge abort test deterministic on Node current
rinatkhaziev Jun 24, 2026
15d4e8e
feat(rechallenge): add --rechallenge-wait opt-in for non-interactive …
rinatkhaziev Jun 30, 2026
054bbc8
fix: purge token/cache even if logout fails
sjinks Jul 1, 2026
e734d78
fix: eslint issues and code smells
sjinks Jul 1, 2026
a00c99c
fix(rechallenge): handle sync downstream errors
sjinks Jul 1, 2026
530098c
fix: SonarSource findings
sjinks Jul 1, 2026
9065f51
test(rechallenge): add api and command coverage
sjinks Jul 1, 2026
18b30b2
docs: document elevated token scope granularity
sjinks Jul 1, 2026
3b31c86
fix: address code review comments
sjinks Jul 1, 2026
a912ed2
fix: address code review comments
sjinks Jul 1, 2026
b217b54
fix: address code review comments
sjinks Jul 1, 2026
57643a0
fix: add `salesforceId` to query for telemetry
sjinks Jul 1, 2026
9329cc3
fix: apply suggestions from code review
sjinks Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions __tests__/bin/vip-app-deploy-rechallenge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { beforeEach, describe, expect, it, jest } from '@jest/globals';

const httpResponse = ( status, body ) => {
return Promise.resolve( {
ok: status >= 200 && status < 300,
status,
headers: {
get: name => ( name.toLowerCase() === 'content-type' ? 'application/json' : null ),
},
json: () => Promise.resolve( body ),
text: () => Promise.resolve( JSON.stringify( body ) ),
} );
};

describe( 'vip app deploy rechallenge smoke', () => {
let appDeployCmd;
let http;
let runRechallenge;
let exitWithError;

beforeEach( () => {
jest.resetModules();

jest.doMock( '../../src/lib/cli/command', () => {
const commandMock = {
argv: () => commandMock,
examples: () => commandMock,
option: () => commandMock,
command: () => commandMock,
};
return jest.fn( () => commandMock );
} );

jest.doMock( '../../src/lib/client-file-uploader', () => ( {
getFileMeta: jest.fn().mockResolvedValue( {
fileName: '/vip/skeleton.zip',
basename: 'skeleton.zip',
fileSize: 123,
isCompressed: true,
} ),
uploadImportFileToS3: jest.fn().mockResolvedValue( {
fileMeta: {
basename: 'skeleton.zip',
fileName: '/vip/skeleton.zip',
fileSize: 123,
isCompressed: true,
},
checksum: 'abc123',
result: 'ok',
} ),
} ) );

jest.doMock( '../../src/lib/custom-deploy/custom-deploy', () => ( {
validateFile: jest.fn().mockResolvedValue( true ),
validateLargeArchiveFiles: jest.fn().mockResolvedValue( true ),
promptToContinue: jest.fn().mockResolvedValue( true ),
validateCustomDeployKey: jest.fn().mockResolvedValue( {
appId: 123,
envId: 456,
envType: 'develop',
envUniqueLabel: 'develop',
primaryDomainName: 'example.com/develop',
launched: false,
} ),
} ) );

jest.doMock( '../../src/lib/tracker', () => ( {
trackEventWithEnv: jest.fn( () => Promise.resolve() ),
} ) );

jest.doMock( '../../src/lib/api/http', () => ( {
__esModule: true,
default: jest.fn(),
} ) );

jest.doMock( '../../src/lib/rechallenge/flow', () => ( {
runRechallenge: jest.fn(),
isInteractiveContext: () => false,
shouldWaitForRechallenge: () => true,
} ) );

jest.doMock( '../../src/lib/rechallenge/token-cache', () => ( {
__esModule: true,
default: {
get: jest.fn( () => Promise.resolve( null ) ),
set: jest.fn( () => Promise.resolve() ),
clearScope: jest.fn(),
clearAll: jest.fn(),
},
} ) );

const exitModule = require( '../../src/lib/cli/exit' );
exitWithError = jest.spyOn( exitModule, 'withError' ).mockImplementation( () => {
throw new Error( 'exit.withError called' );
} );

appDeployCmd = require( '../../src/bin/vip-app-deploy' ).appDeployCmd;
http = require( '../../src/lib/api/http' ).default;
runRechallenge = require( '../../src/lib/rechallenge/flow' ).runRechallenge;

process.env.NODE_ENV = 'test';
process.env.WPVIP_DEPLOY_TOKEN = 'deploy-token';
jest.spyOn( console, 'log' ).mockImplementation( () => {} );
jest.spyOn( console, 'error' ).mockImplementation( () => {} );
} );

it( 'retries app deploy mutation with elevated header in API chain', async () => {
runRechallenge.mockResolvedValueOnce( {
token: 'manual-elevated-token',
expiresAt: new Date( Date.now() + 60_000 ).toISOString(),
purpose: 'validate-elevated-permissions',
} );

http
.mockReturnValueOnce(
httpResponse( 200, {
data: null,
errors: [
{
message: 'Missing elevated token',
extensions: {
code: 'elevated-permission-required',
rechallenge: {
version: 'v2',
createSessionPath: '/rechallenge/v2/sessions',
statusPathTemplate: '/rechallenge/v2/sessions/{challengeId}',
exchangePathTemplate: '/rechallenge/v2/sessions/{challengeId}/exchange',
elevatedHeaderName: 'x-elevated-token',
},
},
},
],
} )
)
.mockReturnValueOnce(
httpResponse( 200, {
data: {
startCustomDeploy: {
success: true,
message: 'ok',
},
},
} )
);

await expect(
appDeployCmd( [ '/vip/skeleton.zip' ], { app: 1, env: 2, force: true } )
).resolves.toBeUndefined();

expect( runRechallenge ).toHaveBeenCalledTimes( 1 );
expect( http ).toHaveBeenCalledTimes( 2 );
const secondCallInit = http.mock.calls[ 1 ][ 1 ];
expect( secondCallInit.headers[ 'x-elevated-token' ] ).toBe( 'manual-elevated-token' );
expect( secondCallInit.headers.authorization ).toBe( 'Bearer deploy-token' );
expect( exitWithError ).not.toHaveBeenCalled();
} );
} );
176 changes: 176 additions & 0 deletions __tests__/bin/vip-defensive-mode-configure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { describe, expect, it, jest, beforeEach } from '@jest/globals';
Comment thread
rinatkhaziev marked this conversation as resolved.

import { defensiveModeConfigureCommand } from '../../src/bin/vip-defensive-mode-configure';
import command from '../../src/lib/cli/command';
import { updateDefensiveModeConfig } from '../../src/lib/defensive-mode/api';
import { trackEvent } from '../../src/lib/tracker';

function mockExit() {
throw 'EXIT';
}
jest.spyOn( console, 'log' ).mockImplementation( () => {} );
jest.spyOn( console, 'error' ).mockImplementation( () => {} );
jest.spyOn( process, 'exit' ).mockImplementation( mockExit );

jest.mock( '../../src/lib/cli/command', () => {
const commandMock = {
argv: () => commandMock,
examples: () => commandMock,
option: () => commandMock,
};
return jest.fn( () => commandMock );
} );

jest.mock( '../../src/lib/defensive-mode/api', () => ( {
updateDefensiveModeConfig: jest.fn( () =>
Promise.resolve( { success: true, message: 'configured' } )
),
appQuery: 'mock-app-query',
} ) );

jest.mock( '../../src/lib/tracker', () => ( {
trackEvent: jest.fn( () => Promise.resolve() ),
} ) );

jest.mock( '../../src/lib/envvar/input', () => ( {
confirm: jest.fn( () => Promise.resolve( true ) ),
} ) );

function baseOpts() {
return {
app: { id: 7, name: 'demo', organization: { id: 1, salesforceId: 'X' } },
env: { id: 9, type: 'develop' },
skipConfirmation: true,
};
}

describe( 'vip defensive-mode configure', () => {
it( 'registers as a command', () => {
expect( command ).toHaveBeenCalled();
} );
} );

describe( 'defensiveModeConfigureCommand', () => {
beforeEach( () => {
jest.clearAllMocks();
} );

it( 'applies full input when all flags are supplied', async () => {
await defensiveModeConfigureCommand( [], {
...baseOpts(),
enabled: 'true',
challengeType: '1',
connectionThresholdAbsolute: '1000',
connectionThresholdPercentage: '50',
} );
expect( updateDefensiveModeConfig ).toHaveBeenCalledWith( {
appId: 7,
envId: 9,
enabled: true,
challengeType: 1,
connectionThresholdAbsolute: 1000,
connectionThresholdPercentage: 50,
} );
} );

it( 'errors when required flags missing in non-interactive mode', async () => {
await expect(
defensiveModeConfigureCommand( [], {
...baseOpts(),
nonInteractive: true,
} )
).rejects.toBe( 'EXIT' );
expect( updateDefensiveModeConfig ).not.toHaveBeenCalled();
} );

it( 'rejects non-boolean enabled values', async () => {
await expect(
defensiveModeConfigureCommand( [], {
...baseOpts(),
enabled: 'maybe',
challengeType: '1',
nonInteractive: true,
} )
).rejects.toBe( 'EXIT' );
} );

it( 'rejects non-integer challenge-type', async () => {
await expect(
defensiveModeConfigureCommand( [], {
...baseOpts(),
enabled: 'true',
challengeType: 'oops',
nonInteractive: true,
} )
).rejects.toBe( 'EXIT' );
} );

it( 'tracks success', async () => {
await defensiveModeConfigureCommand( [], {
...baseOpts(),
enabled: 'false',
challengeType: '1',
} );
expect( trackEvent ).toHaveBeenCalledWith(
'defensive_mode_configure_command_success',
expect.any( Object )
);
} );

it( 'logs the proposed configuration before mutating', async () => {
const consoleSpy = jest.spyOn( console, 'log' );
await defensiveModeConfigureCommand( [], {
...baseOpts(),
enabled: 'true',
challengeType: '2',
} );
const allArgs = consoleSpy.mock.calls.flat().filter( arg => typeof arg === 'string' );
const settingsTable = allArgs.find( arg => arg.includes( 'Challenge type' ) );
expect( settingsTable ).toBeDefined();
expect( settingsTable ).toContain( 'Enabled' );
expect( settingsTable ).toContain( 'true' );
expect( settingsTable ).toContain( '2' );
expect( settingsTable ).toContain( '(not specified)' );
} );

it( 'rejects bare threshold flags (boolean true)', async () => {
await expect(
defensiveModeConfigureCommand( [], {
...baseOpts(),
enabled: 'true',
challengeType: '1',
connectionThresholdAbsolute: true,
nonInteractive: true,
} )
).rejects.toBe( 'EXIT' );
expect( updateDefensiveModeConfig ).not.toHaveBeenCalled();
} );

it( 'refuses production mutation in non-interactive mode without --skip-confirmation', async () => {
await expect(
defensiveModeConfigureCommand( [], {
...baseOpts(),
env: { id: 9, type: 'production' },
skipConfirmation: false,
nonInteractive: true,
enabled: 'false',
challengeType: '1',
} )
).rejects.toBe( 'EXIT' );
expect( updateDefensiveModeConfig ).not.toHaveBeenCalled();
} );

it( 'allows production mutation in non-interactive mode with --skip-confirmation', async () => {
await defensiveModeConfigureCommand( [], {
...baseOpts(),
env: { id: 9, type: 'production' },
skipConfirmation: true,
nonInteractive: true,
enabled: 'false',
challengeType: '1',
} );
expect( updateDefensiveModeConfig ).toHaveBeenCalledWith(
expect.objectContaining( { envId: 9, enabled: false, challengeType: 1 } )
);
} );
} );
Loading