Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .depcheckrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ ignores:
# Include an explanation why the package is ignored
- '@babel/*' # Used in babel.config.js and babel.config.jest.js
- '@types/react-dom' # Used in src/index.jsx
- anypoint-cli-v4 # CLI tool executed via execSync in scripts, not imported
- jest-environment-jsdom-sixteen # Used by test:react script in package.json
- lib # Used in App.jsx, references local directory
- node-fetch # False positive - clientConfig.ts imports the type from @types/node-fetch
Expand Down
18 changes: 18 additions & 0 deletions Contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ $ yarn test

### Merging to `preview`

### Updating APIs
Update the version file in `api-versions.txt` to the version you want to test or add a new API. Use the API version from your API release or branch build.

To add a new API:

1. Edit `api-versions.txt` and add a new line following the existing format:
- `<api-asset-id-with-version-suffix>=<semantic-version>`
- Example:

```
shopper-newfeature-oas-v1=1.0.0
```

2. Run:
- `yarn updateApis` to download the API OAS into `apis/` directory

#### Update and Check SDK before merging:

Before merging any changes into `preview`, SDK generation must pass locally:

```
Expand Down
14 changes: 14 additions & 0 deletions api-versions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
shopper-baskets-oas-v1=1.9.1
shopper-baskets-oas-v2=2.1.1
shopper-consents-oas-v1=1.1.4
shopper-context-oas-v1=1.1.3
shopper-customers-oas-v1=1.2.1
shopper-experience-oas-v1=1.1.2
shopper-gift-certificates-oas-v1=1.0.29
shopper-login-oas-v1=1.42.2
shopper-orders-oas-v1=1.6.0
shopper-products-oas-v1=1.1.2
shopper-promotions-oas-v1=1.0.39
shopper-search-oas-v1=1.5.4
shopper-seo-oas-v1=1.0.16
shopper-stores-oas-v1=1.0.19
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
'scripts/**/*.{js,jsx,ts,tsx}',
'!scripts/generate.ts',
'!scripts/updateApis.ts',
'!scripts/utils.ts',
'!scripts/generateVersionTable.ts',
'!<rootDir>/node_modules/',
],
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"test": "yarn run check:types && yarn run test:unit && CI=true yarn run test:react",
"test:react": "react-scripts test --env=jest-environment-jsdom-sixteen src/environment",
"test:unit": "jest --coverage --testPathIgnorePatterns node_modules src/environment --silent",
"updateApis": "ts-node --compiler-options '{\"module\": \"commonjs\", \"target\": \"ES6\" }' ./scripts/updateApis.ts && yarn diffApis"
"updateApis": "ts-node --compiler-options '{\"module\": \"commonjs\", \"target\": \"ES6\" }' scripts/updateApis.ts && yarn diffApis"
},
"husky": {
"hooks": {
Expand Down Expand Up @@ -93,7 +93,6 @@
"resolutions": {
"**/@npmcli/fs": "<1.1.0",
"**/@oclif/command": "<=1.8.3",
"**/@oclif/core": "<=0.5.9",
"**/isbot": "<=3.0.27",
"**/yargs": "<17",
"depcheck/@babel/parser": "7.16.4"
Expand All @@ -117,12 +116,15 @@
"@rollup/plugin-node-resolve": "8.4.0",
"@testing-library/jest-dom": "5.16.4",
"@testing-library/react": "10.4.9",
"@types/adm-zip": "^0.5.0",
"@types/fs-extra": "^9.0.13",
"@types/node-fetch": "^2.6.2",
"@types/react-dom": "^16.9.16",
"@types/seedrandom": "^3.0.8",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"adm-zip": "^0.5.10",
"anypoint-cli-v4": "1.6.12",
"autoprefixer": "9.8.8",
"bundlesize2": "^0.0.31",
"depcheck": "^1.4.3",
Expand Down Expand Up @@ -183,7 +185,7 @@
},
{
"path": "commerce-sdk-isomorphic-with-deps.tgz",
"maxSize": "2.5 MB"
"maxSize": "2.65 MB"
}
],
"proxy": "https://SHORTCODE.api.commercecloud.salesforce.com"
Expand Down
269 changes: 269 additions & 0 deletions scripts/download-apis.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import {execSync} from 'child_process';
import fs from 'fs-extra';
import path from 'path';
import AdmZip from 'adm-zip';
import {downloadApisWithAnypointCli} from './download-apis';

// Mock dependencies before importing the module
jest.mock('child_process');
jest.mock('fs-extra');
jest.mock('adm-zip');

const mockedExecSync = execSync as jest.MockedFunction<typeof execSync>;
const mockedFs = fs as jest.Mocked<typeof fs>;
const MockedAdmZip = AdmZip as jest.MockedClass<typeof AdmZip>;

describe('download-apis', () => {
const mockApiId =
'893f605e-10e2-423a-bdb4-f952f56eb6d8/shopper-baskets-oas/1.9.0';
const mockTargetDir = '/path/to/target';
const mockOrgId = '893f605e-10e2-423a-bdb4-f952f56eb6d8';
const mockTempDir = path.join(process.cwd(), 'temp', 'downloads');

beforeEach(() => {
jest.clearAllMocks();
process.env.ANYPOINT_USERNAME = 'test-user';
process.env.ANYPOINT_PASSWORD = 'test-pass';

// Mock console methods
jest.spyOn(console, 'log').mockImplementation();
jest.spyOn(console, 'error').mockImplementation();

// Mock file system operations
mockedFs.readdir.mockResolvedValue(['api-asset.zip'] as any);

Check warning on line 39 in scripts/download-apis.test.ts

View workflow job for this annotation

GitHub Actions / linux-tests (22)

Unexpected any. Specify a different type

Check warning on line 39 in scripts/download-apis.test.ts

View workflow job for this annotation

GitHub Actions / linux-tests (20)

Unexpected any. Specify a different type
mockedFs.remove.mockResolvedValue(undefined as void);

// Mock execSync
mockedExecSync.mockReturnValue(Buffer.from(''));

// Mock AdmZip
const mockZipInstance = {
extractAllTo: jest.fn(),
} as unknown as AdmZip;
MockedAdmZip.mockImplementation(() => mockZipInstance);
});

afterEach(() => {
delete process.env.ANYPOINT_USERNAME;
delete process.env.ANYPOINT_PASSWORD;
jest.restoreAllMocks();
});

describe('downloadApisWithAnypointCli', () => {
it('should successfully download and extract API', async () => {
await downloadApisWithAnypointCli(mockApiId, mockTargetDir, mockOrgId);

// Verify temp directory was created
expect(mockedFs.ensureDir).toHaveBeenCalledWith(mockTempDir);

// Verify anypoint-cli command was executed
expect(mockedExecSync).toHaveBeenCalledWith(
expect.stringContaining('anypoint-cli-v4 exchange:asset:download'),
expect.objectContaining({
stdio: 'inherit',
cwd: process.cwd(),
env: process.env,
})
);

// Verify command includes credentials and org ID
expect(mockedExecSync).toHaveBeenCalledWith(
expect.stringContaining("--username 'test-user'"),
expect.any(Object)
);
expect(mockedExecSync).toHaveBeenCalledWith(
expect.stringContaining("--password 'test-pass'"),
expect.any(Object)
);
expect(mockedExecSync).toHaveBeenCalledWith(
expect.stringContaining(`--organization=${mockOrgId}`),
expect.any(Object)
);

// Verify zip file was read
expect(mockedFs.readdir).toHaveBeenCalledWith(mockTempDir);

// Verify target directory was created
expect(mockedFs.ensureDir).toHaveBeenCalledWith(mockTargetDir);

// Verify zip was extracted
expect(MockedAdmZip).toHaveBeenCalledWith(
path.join(mockTempDir, 'api-asset.zip')
);

// Verify temp directory was cleaned up
expect(mockedFs.remove).toHaveBeenCalledWith(mockTempDir);

// eslint-disable-next-line no-console
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('Successfully downloaded and extracted')
);
});

it('should handle empty credentials gracefully', async () => {
delete process.env.ANYPOINT_USERNAME;
delete process.env.ANYPOINT_PASSWORD;

await downloadApisWithAnypointCli(mockApiId, mockTargetDir, mockOrgId);

expect(mockedExecSync).toHaveBeenCalledWith(
expect.stringContaining("--username ''"),
expect.any(Object)
);
expect(mockedExecSync).toHaveBeenCalledWith(
expect.stringContaining("--password ''"),
expect.any(Object)
);
});

it('should throw error when anypoint-cli command fails', async () => {
mockedExecSync.mockImplementation(() => {
throw new Error('Command execution failed');
});

await expect(
downloadApisWithAnypointCli(mockApiId, mockTargetDir, mockOrgId)
).rejects.toThrow(
'Failed to download API 893f605e-10e2-423a-bdb4-f952f56eb6d8/shopper-baskets-oas/1.9.0: potential reasons: api or version does not exist, wrong credentials, wrong organization ID'
);

// Verify temp directory was still created
expect(mockedFs.ensureDir).toHaveBeenCalledWith(mockTempDir);
});

it('should throw error when no zip file is found', async () => {
mockedFs.readdir.mockResolvedValue(['not-a-zip.txt', 'readme.md'] as any);

Check warning on line 141 in scripts/download-apis.test.ts

View workflow job for this annotation

GitHub Actions / linux-tests (22)

Unexpected any. Specify a different type

Check warning on line 141 in scripts/download-apis.test.ts

View workflow job for this annotation

GitHub Actions / linux-tests (20)

Unexpected any. Specify a different type

await expect(
downloadApisWithAnypointCli(mockApiId, mockTargetDir, mockOrgId)
).rejects.toThrow(
`Failed to download API ${mockApiId}: No zip file found in ${mockTempDir}`
);
});

it('should find zip file among multiple files', async () => {
mockedFs.readdir.mockResolvedValue([
'readme.md',
'api-asset.zip',
'other-file.txt',
] as any);

Check warning on line 155 in scripts/download-apis.test.ts

View workflow job for this annotation

GitHub Actions / linux-tests (22)

Unexpected any. Specify a different type

Check warning on line 155 in scripts/download-apis.test.ts

View workflow job for this annotation

GitHub Actions / linux-tests (20)

Unexpected any. Specify a different type

await downloadApisWithAnypointCli(mockApiId, mockTargetDir, mockOrgId);

expect(MockedAdmZip).toHaveBeenCalledWith(
path.join(mockTempDir, 'api-asset.zip')
);
});

it('should handle errors during zip extraction', async () => {
const mockZipInstance = {
extractAllTo: jest.fn(() => {
throw new Error('Extraction failed');
}),
} as unknown as AdmZip;
MockedAdmZip.mockImplementation(() => mockZipInstance);

await expect(
downloadApisWithAnypointCli(mockApiId, mockTargetDir, mockOrgId)
).rejects.toThrow(
'Failed to download API 893f605e-10e2-423a-bdb4-f952f56eb6d8/shopper-baskets-oas/1.9.0: Extraction failed'
);
});

it('should handle errors during cleanup', async () => {
mockedFs.remove.mockRejectedValueOnce(new Error('Cleanup failed'));

await expect(
downloadApisWithAnypointCli(mockApiId, mockTargetDir, mockOrgId)
).rejects.toThrow(
'Failed to download API 893f605e-10e2-423a-bdb4-f952f56eb6d8/shopper-baskets-oas/1.9.0: Cleanup failed'
);
});

it('should handle non-Error exceptions from execSync', async () => {
mockedExecSync.mockImplementation(() => {
// eslint-disable-next-line no-throw-literal
throw 'String error';
});

await expect(
downloadApisWithAnypointCli(mockApiId, mockTargetDir, mockOrgId)
).rejects.toThrow(
'Failed to download API 893f605e-10e2-423a-bdb4-f952f56eb6d8/shopper-baskets-oas/1.9.0: potential reasons: api or version does not exist, wrong credentials, wrong organization ID'
);
});

it('should handle non-Error exceptions from other operations', async () => {
mockedFs.readdir.mockImplementation(() => {
// eslint-disable-next-line no-throw-literal
throw 'Non-error exception';
});

await expect(
downloadApisWithAnypointCli(mockApiId, mockTargetDir, mockOrgId)
).rejects.toThrow(`Failed to download API ${mockApiId}`);
});

it('should use correct temp directory path', async () => {
const expectedTempDir = path.join(process.cwd(), 'temp', 'downloads');

await downloadApisWithAnypointCli(mockApiId, mockTargetDir, mockOrgId);

expect(mockedFs.ensureDir).toHaveBeenCalledWith(expectedTempDir);
expect(mockedExecSync).toHaveBeenCalledWith(
expect.stringContaining(expectedTempDir),
expect.any(Object)
);
});

it('should pass correct parameters to anypoint-cli command', async () => {
await downloadApisWithAnypointCli(mockApiId, mockTargetDir, mockOrgId);

const expectedCmd = `anypoint-cli-v4 exchange:asset:download ${mockApiId} ${mockTempDir} --username 'test-user' --password 'test-pass' --organization=${mockOrgId}`;

expect(mockedExecSync).toHaveBeenCalledWith(
expectedCmd,
expect.objectContaining({
stdio: 'inherit',
cwd: process.cwd(),
env: process.env,
})
);
});

it('should log download progress', async () => {
await downloadApisWithAnypointCli(mockApiId, mockTargetDir, mockOrgId);

// eslint-disable-next-line no-console
expect(console.log).toHaveBeenCalledWith(
`Downloading API ${mockApiId} using anypoint-cli...`
);
// eslint-disable-next-line no-console
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('Extracting api-asset.zip')
);
// eslint-disable-next-line no-console
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('Successfully downloaded and extracted')
);
});

it('should extract zip with overwrite flag', async () => {
const mockExtractAllTo = jest.fn();
const mockZipInstance = {
extractAllTo: mockExtractAllTo,
} as unknown as AdmZip;
MockedAdmZip.mockImplementation(() => mockZipInstance);

await downloadApisWithAnypointCli(mockApiId, mockTargetDir, mockOrgId);

expect(mockExtractAllTo).toHaveBeenCalledWith(mockTargetDir, true);
});
});
});
Loading
Loading