Skip to content

Commit d9b8ddf

Browse files
authored
Fix test isolation, fast polling, and cleanup of command tests (#51)
* Fix test isolation, fast polling, and cleanup low-value command tests Test Isolation: - Add `credentialsFile` parameter to ResolveConfigOptions and LoadConfigOptions - Update MobifySource to use credentialsFile when provided (overrides ~/.mobify) - Add --credentials-file flag with MRT_CREDENTIALS_FILE env var to MrtCommand - Add config-isolation.ts helper to clear SFCC_*/MRT_* env vars for tests Fast Polling Tests: - Use short pollInterval for site-archive tests instead of default 3000ms - Tests now complete in milliseconds instead of seconds Command Test Cleanup: - Delete cartridge-command.test.ts (100% trivial delegation tests) - Simplify base-command.test.ts (keep getExtraParams, catch tests) - Simplify instance-command.test.ts (keep requireX, context tests) - Simplify mrt-command.test.ts (keep requireMrtCredentials only) - Simplify oauth-command.test.ts (keep parseAuthMethods, requireOAuthCredentials) - Simplify ods-command.test.ts (keep odsClient lazy init tests) Documentation: - Update testing skill with config isolation, pollInterval patterns - Add command test guidelines (what to test vs avoid) - Remove general knowledge content (basic Mocha/Chai patterns) * update lockfile * Remove upward search from dw.json loading, complete test isolation - Change loadDwJson() default to look only at ./dw.json (no parent search) - Update DwJsonSource to use same logic, remove findDwJson import - Keep findDwJson() exported for users who need explicit upward search - Set SFCC_CONFIG=/dev/null and MRT_CREDENTIALS_FILE=/dev/null in isolateConfig() - Update test to verify new behavior (no upward search) * Wire up isolateConfig to SDK command tests Add isolateConfig()/restoreConfig() to command test files to ensure tests are isolated from developer's environment variables (SFCC_*, MRT_*). Files updated: - test/cli/base-command.test.ts - test/cli/instance-command.test.ts - test/cli/mrt-command.test.ts - test/cli/oauth-command.test.ts - test/cli/ods-command.test.ts * timing issues in tests; don't sleep is poll is 0 * initial wait from poll interval * Improve command test patterns with Sinon and integration tests - Add sinon, @types/sinon, @oclif/test as SDK dev dependencies - Create stubParse helper for cleaner parse method mocking - Refactor 5 command test files to use Sinon instead of manual mocking - Add test fixture (test/fixtures/test-cli/) for integration testing - Add base-command.integration.test.ts with runCommand() tests - Update testing skill docs with new patterns The stubParse helper eliminates the brittle MockableXxxCommand type casting pattern. Integration tests exercise full command lifecycle through the oclif test utilities. * Restore CartridgeCommand unit tests with improved coverage - Add cartridge-command.test.ts with tests for cartridgePath, cartridgeOptions, provider runner init, and findCartridgesWithProviders - Use stubParse helper with server mock for instance-dependent tests - Improves cartridge-command.ts coverage from 19% to 91% * Add InstanceCommand and MrtCommand integration tests Integration tests provide API contract validation beyond code coverage: - Catch flag definition errors (wrong type, missing env var) - Validate baseFlags inheritance works correctly - Exercise full oclif command lifecycle (discovery, parse, init, run) - Test commands the way consumers actually use them New fixtures: - test-instance.js: Tests server, instance flags and hasServer check - test-mrt.js: Tests api-key, project, environment, cloud-origin flags New integration tests: - instance-command.integration.test.ts (5 tests) - mrt-command.integration.test.ts (8 tests) Total integration tests: 18 (BaseCommand + InstanceCommand + MrtCommand) * prettier updates
1 parent 8cc19aa commit d9b8ddf

31 files changed

+1053
-2016
lines changed

.claude/skills/testing/SKILL.md

Lines changed: 152 additions & 223 deletions
Large diffs are not rendered by default.

packages/b2c-cli/src/commands/ods/create.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> {
254254
this.log(t('commands.ods.create.waiting', 'Waiting for sandbox to be ready...'));
255255

256256
// Initial delay before first poll to allow the sandbox to be registered in the API
257-
await this.sleep(2000);
257+
await this.sleep(pollIntervalMs);
258258

259259
while (true) {
260260
// Check for timeout

packages/b2c-tooling-sdk/.c8rc.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
{
22
"all": true,
33
"src": ["src"],
4-
"exclude": [
5-
"src/clients/*.generated.ts",
6-
"test/**",
7-
"**/*.d.ts",
8-
"src/**/*types.ts"
9-
],
4+
"exclude": ["src/clients/*.generated.ts", "test/**", "**/*.d.ts", "src/**/*types.ts"],
105
"reporter": ["text", "text-summary", "html", "lcov"],
116
"report-dir": "coverage",
127
"check-coverage": true,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
*.generated.ts
2+
dist
3+
coverage
4+
data
5+
specs

packages/b2c-tooling-sdk/README.md

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ npm install @salesforce/b2c-tooling-sdk
2020
Use `resolveConfig()` to load configuration from project files (dw.json) and create a B2C instance:
2121

2222
```typescript
23-
import { resolveConfig } from '@salesforce/b2c-tooling-sdk/config';
23+
import {resolveConfig} from '@salesforce/b2c-tooling-sdk/config';
2424

2525
// Load configuration, override secrets from environment
2626
const config = resolveConfig({
@@ -36,8 +36,8 @@ await instance.webdav.mkcol('Cartridges/v1');
3636
await instance.webdav.put('Cartridges/v1/app.zip', zipBuffer);
3737

3838
// Use typed OCAPI client (openapi-fetch)
39-
const { data, error } = await instance.ocapi.GET('/sites', {
40-
params: { query: { select: '(**)' } },
39+
const {data, error} = await instance.ocapi.GET('/sites', {
40+
params: {query: {select: '(**)'}},
4141
});
4242
```
4343

@@ -46,16 +46,16 @@ const { data, error } = await instance.ocapi.GET('/sites', {
4646
For advanced use cases, you can construct a B2CInstance directly:
4747

4848
```typescript
49-
import { B2CInstance } from '@salesforce/b2c-tooling-sdk';
49+
import {B2CInstance} from '@salesforce/b2c-tooling-sdk';
5050

5151
const instance = new B2CInstance(
52-
{ hostname: 'your-sandbox.demandware.net', codeVersion: 'v1' },
52+
{hostname: 'your-sandbox.demandware.net', codeVersion: 'v1'},
5353
{
5454
oauth: {
5555
clientId: 'your-client-id',
56-
clientSecret: 'your-client-secret'
57-
}
58-
}
56+
clientSecret: 'your-client-secret',
57+
},
58+
},
5959
);
6060
```
6161

@@ -89,24 +89,24 @@ The OCAPI client uses [openapi-fetch](https://openapi-ts.dev/openapi-fetch/) wit
8989

9090
```typescript
9191
// List sites
92-
const { data, error } = await instance.ocapi.GET('/sites', {
93-
params: { query: { select: '(**)' } },
92+
const {data, error} = await instance.ocapi.GET('/sites', {
93+
params: {query: {select: '(**)'}},
9494
});
9595

9696
// Activate a code version
97-
const { data, error } = await instance.ocapi.PATCH('/code_versions/{code_version_id}', {
98-
params: { path: { code_version_id: 'v1' } },
99-
body: { active: true },
97+
const {data, error} = await instance.ocapi.PATCH('/code_versions/{code_version_id}', {
98+
params: {path: {code_version_id: 'v1'}},
99+
body: {active: true},
100100
});
101101
```
102102

103103
### Code Deployment
104104

105105
```typescript
106-
import { findAndDeployCartridges, activateCodeVersion } from '@salesforce/b2c-tooling-sdk/operations/code';
106+
import {findAndDeployCartridges, activateCodeVersion} from '@salesforce/b2c-tooling-sdk/operations/code';
107107

108108
// Deploy cartridges
109-
await findAndDeployCartridges(instance, './cartridges', { reload: true });
109+
await findAndDeployCartridges(instance, './cartridges', {reload: true});
110110

111111
// Activate code version
112112
await activateCodeVersion(instance, 'v1');
@@ -115,7 +115,7 @@ await activateCodeVersion(instance, 'v1');
115115
### Job Execution
116116

117117
```typescript
118-
import { executeJob, waitForJob, siteArchiveImport } from '@salesforce/b2c-tooling-sdk/operations/jobs';
118+
import {executeJob, waitForJob, siteArchiveImport} from '@salesforce/b2c-tooling-sdk/operations/jobs';
119119

120120
// Run a job and wait for completion
121121
const execution = await executeJob(instance, 'my-job-id');
@@ -129,32 +129,32 @@ await siteArchiveImport(instance, './site-data.zip');
129129

130130
The SDK provides subpath exports for tree-shaking and organization:
131131

132-
| Export | Description |
133-
|--------|-------------|
134-
| `@salesforce/b2c-tooling-sdk` | Main entry point with all exports |
135-
| `@salesforce/b2c-tooling-sdk/config` | Configuration resolution (resolveConfig) |
136-
| `@salesforce/b2c-tooling-sdk/auth` | Authentication strategies (OAuth, Basic, API Key) |
137-
| `@salesforce/b2c-tooling-sdk/instance` | B2CInstance class |
138-
| `@salesforce/b2c-tooling-sdk/clients` | Low-level API clients (WebDAV, OCAPI, SLAS, ODS, MRT) |
139-
| `@salesforce/b2c-tooling-sdk/operations/code` | Code deployment operations |
140-
| `@salesforce/b2c-tooling-sdk/operations/jobs` | Job execution and site import/export |
141-
| `@salesforce/b2c-tooling-sdk/operations/sites` | Site management |
142-
| `@salesforce/b2c-tooling-sdk/discovery` | Workspace type detection (PWA Kit, SFRA, etc.) |
143-
| `@salesforce/b2c-tooling-sdk/cli` | CLI utilities (BaseCommand, table rendering) |
144-
| `@salesforce/b2c-tooling-sdk/logging` | Structured logging utilities |
132+
| Export | Description |
133+
| ---------------------------------------------- | ----------------------------------------------------- |
134+
| `@salesforce/b2c-tooling-sdk` | Main entry point with all exports |
135+
| `@salesforce/b2c-tooling-sdk/config` | Configuration resolution (resolveConfig) |
136+
| `@salesforce/b2c-tooling-sdk/auth` | Authentication strategies (OAuth, Basic, API Key) |
137+
| `@salesforce/b2c-tooling-sdk/instance` | B2CInstance class |
138+
| `@salesforce/b2c-tooling-sdk/clients` | Low-level API clients (WebDAV, OCAPI, SLAS, ODS, MRT) |
139+
| `@salesforce/b2c-tooling-sdk/operations/code` | Code deployment operations |
140+
| `@salesforce/b2c-tooling-sdk/operations/jobs` | Job execution and site import/export |
141+
| `@salesforce/b2c-tooling-sdk/operations/sites` | Site management |
142+
| `@salesforce/b2c-tooling-sdk/discovery` | Workspace type detection (PWA Kit, SFRA, etc.) |
143+
| `@salesforce/b2c-tooling-sdk/cli` | CLI utilities (BaseCommand, table rendering) |
144+
| `@salesforce/b2c-tooling-sdk/logging` | Structured logging utilities |
145145

146146
## Logging
147147

148148
Configure logging for debugging HTTP requests:
149149

150150
```typescript
151-
import { configureLogger } from '@salesforce/b2c-tooling-sdk/logging';
151+
import {configureLogger} from '@salesforce/b2c-tooling-sdk/logging';
152152

153153
// Enable debug logging (shows HTTP request summaries)
154-
configureLogger({ level: 'debug' });
154+
configureLogger({level: 'debug'});
155155

156156
// Enable trace logging (shows full request/response with headers and bodies)
157-
configureLogger({ level: 'trace' });
157+
configureLogger({level: 'trace'});
158158
```
159159

160160
## Documentation

packages/b2c-tooling-sdk/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,8 @@
195195
"build:cjs": "tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
196196
"clean": "shx rm -rf dist",
197197
"lint": "eslint",
198-
"format": "prettier --write src scripts",
199-
"format:check": "prettier --check src scripts",
198+
"format": "prettier --write .",
199+
"format:check": "prettier --check .",
200200
"pretest": "tsc --noEmit -p test",
201201
"test": "c8 mocha --forbid-only \"test/**/*.test.ts\"",
202202
"test:ci": "c8 mocha --forbid-only --reporter json --reporter-option output=test-results.json \"test/**/*.test.ts\"",
@@ -210,12 +210,14 @@
210210
"@eslint/compat": "^1",
211211
"@oclif/core": "^4",
212212
"@oclif/prettier-config": "^0.2.1",
213+
"@oclif/test": "^4.1.14",
213214
"@salesforce/dev-config": "^4.3.2",
214215
"@tony.ganchev/eslint-plugin-header": "^3.1.11",
215216
"@types/archiver": "^7.0.0",
216217
"@types/chai": "^4.3.20",
217218
"@types/mocha": "^10.0.10",
218219
"@types/node": "^18.19.130",
220+
"@types/sinon": "^21.0.0",
219221
"@types/xml2js": "^0.4.14",
220222
"c8": "^10.1.3",
221223
"chai": "^4.5.0",
@@ -227,6 +229,7 @@
227229
"openapi-typescript": "^7.10.1",
228230
"prettier": "^3.6.2",
229231
"shx": "^0.3.3",
232+
"sinon": "^21.0.1",
230233
"tsx": "^4.20.6",
231234
"typescript": "^5",
232235
"typescript-eslint": "^8"
@@ -244,9 +247,9 @@
244247
},
245248
"dependencies": {
246249
"archiver": "^7.0.1",
247-
"fuse.js": "^7.0.0",
248250
"chokidar": "^5.0.0",
249251
"cliui": "^9.0.1",
252+
"fuse.js": "^7.0.0",
250253
"glob": "^13.0.0",
251254
"i18next": "^25.6.3",
252255
"jszip": "^3.10.1",

packages/b2c-tooling-sdk/src/cli/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export interface LoadConfigOptions {
4444
configPath?: string;
4545
/** Cloud origin for MRT ~/.mobify lookup (e.g., https://cloud-staging.mobify.com) */
4646
cloudOrigin?: string;
47+
/** Path to custom MRT credentials file (overrides default ~/.mobify) */
48+
credentialsFile?: string;
4749
}
4850

4951
/**
@@ -110,6 +112,7 @@ export function loadConfig(
110112
configPath: options.configPath,
111113
hostnameProtection: true,
112114
cloudOrigin: options.cloudOrigin,
115+
credentialsFile: options.credentialsFile,
113116
sourcesBefore: pluginSources.before,
114117
sourcesAfter: pluginSources.after,
115118
});

packages/b2c-tooling-sdk/src/cli/mrt-command.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,21 @@ export abstract class MrtCommand<T extends typeof Command> extends BaseCommand<T
5555
description: `MRT cloud origin URL (default: ${DEFAULT_MRT_ORIGIN})`,
5656
env: 'SFCC_MRT_CLOUD_ORIGIN',
5757
}),
58+
'credentials-file': Flags.string({
59+
description: 'Path to MRT credentials file (overrides default ~/.mobify)',
60+
env: 'MRT_CREDENTIALS_FILE',
61+
}),
5862
};
5963

6064
protected override loadConfiguration(): ResolvedConfig {
6165
const cloudOrigin = this.flags['cloud-origin'] as string | undefined;
66+
const credentialsFile = this.flags['credentials-file'] as string | undefined;
6267

6368
const options: LoadConfigOptions = {
6469
instance: this.flags.instance,
6570
configPath: this.flags.config,
6671
cloudOrigin, // MobifySource uses this to load ~/.mobify--[hostname] if set
72+
credentialsFile, // Override path to MRT credentials file
6773
};
6874

6975
const flagConfig: Partial<ResolvedConfig> = {

packages/b2c-tooling-sdk/src/config/dw-json.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,26 +149,32 @@ function selectConfig(json: DwJsonMultiConfig, instanceName?: string): DwJsonCon
149149
/**
150150
* Loads configuration from a dw.json file.
151151
*
152-
* Searches upward from the current directory (or specified startDir) for a dw.json file.
153-
* Supports both single-config and multi-config formats.
152+
* If an explicit path is provided, uses that file. Otherwise, looks for dw.json
153+
* in the startDir (or cwd). Does NOT search parent directories.
154+
*
155+
* Use `findDwJson()` if you need to search upward through parent directories.
154156
*
155157
* @param options - Loading options
156158
* @returns The parsed config, or undefined if no dw.json found
157159
*
158160
* @example
159-
* // Auto-find dw.json
161+
* // Load from ./dw.json (current directory)
160162
* const config = loadDwJson();
161163
*
164+
* // Load from specific directory
165+
* const config = loadDwJson({ startDir: '/path/to/project' });
166+
*
162167
* // Use named instance
163168
* const config = loadDwJson({ instance: 'staging' });
164169
*
165170
* // Explicit path
166171
* const config = loadDwJson({ path: './config/dw.json' });
167172
*/
168173
export function loadDwJson(options: LoadDwJsonOptions = {}): DwJsonConfig | undefined {
169-
const dwJsonPath = options.path || findDwJson(options.startDir);
174+
// If explicit path provided, use it. Otherwise default to ./dw.json (no upward search)
175+
const dwJsonPath = options.path ?? path.join(options.startDir || process.cwd(), 'dw.json');
170176

171-
if (!dwJsonPath || !fs.existsSync(dwJsonPath)) {
177+
if (!fs.existsSync(dwJsonPath)) {
172178
return undefined;
173179
}
174180

packages/b2c-tooling-sdk/src/config/sources/dw-json-source.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
*
99
* @internal This module is internal to the SDK. Use ConfigResolver instead.
1010
*/
11-
import {loadDwJson, findDwJson} from '../dw-json.js';
11+
import * as path from 'node:path';
12+
import {loadDwJson} from '../dw-json.js';
1213
import {mapDwJsonToNormalizedConfig} from '../mapping.js';
1314
import type {ConfigSource, NormalizedConfig, ResolveConfigOptions} from '../types.js';
1415

@@ -33,8 +34,8 @@ export class DwJsonSource implements ConfigSource {
3334
return undefined;
3435
}
3536

36-
// Track the path for diagnostics
37-
this.lastPath = options.configPath || findDwJson(options.startDir);
37+
// Track the path for diagnostics - use explicit path or default location
38+
this.lastPath = options.configPath ?? path.join(options.startDir || process.cwd(), 'dw.json');
3839

3940
return mapDwJsonToNormalizedConfig(dwConfig);
4041
}

0 commit comments

Comments
 (0)