Skip to content

Commit 024943e

Browse files
committed
Update dryrun
1 parent 281866a commit 024943e

File tree

3 files changed

+262
-117
lines changed

3 files changed

+262
-117
lines changed

docs/implementation-guides/building-integrations/testing.mdx

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ nango dryrun fetch-tickets abc-123 --save
139139

140140
### Mock file structure
141141

142-
Mocks are saved in a single `.test.json` file alongside your test file:
142+
Mocks are saved in a single `.test.json` file that can be used alongside your test file:
143143

144144
```
145145
github/
@@ -152,8 +152,8 @@ The `test.json` file contains all the necessary mock data for a given test:
152152

153153
```json
154154
{
155-
"input": { "title": "Test ticket" }, // Action input
156-
"output": { "id": "TKT-123", "status": "created" }, // Action output
155+
"input": { "title": "Test ticket" },
156+
"output": { "id": "TKT-123", "status": "created" },
157157
"nango": {
158158
"getConnection": { "connectionId": "abc-123", "provider": "github" },
159159
"getMetadata": { "accountId": "test-123" },
@@ -179,22 +179,6 @@ The `test.json` file contains all the necessary mock data for a given test:
179179
}
180180
```
181181

182-
### Migrating from the old format
183-
184-
If you have tests using the old multi-file mock format, you can automatically migrate them to the new unified format.
185-
186-
Set the `MIGRATE_MOCKS` environment variable to `true` and run your tests:
187-
188-
```bash
189-
MIGRATE_MOCKS=true npm test
190-
```
191-
192-
This will:
193-
1. Run your tests using the old mock files.
194-
2. Intercept all mock data accessed during the test run.
195-
3. Save the data into a new `.test.json` file.
196-
4. The old mock directory can then be safely deleted.
197-
198182
### Using stubbed metadata
199183

200184
For syncs that rely on connection metadata, you can provide test metadata:
@@ -330,18 +314,34 @@ describe('github create-ticket tests', () => {
330314

331315
### How mocks work in tests
332316

333-
The `NangoSyncMock` and `NangoActionMock` classes automatically load your saved mocks:
317+
The `NangoSyncMock` and `NangoActionMock` classes automatically load your saved mocks from the `.test.json` file:
334318

335-
1. **API requests** are intercepted and return saved mock responses
336-
2. **Input data** is loaded from `input.json` (for actions)
337-
3. **Expected outputs** are loaded from the appropriate mock files
338-
4. **Tests compare** actual outputs against expected outputs
319+
1. **API requests** are intercepted and return saved mock responses from the `api` section.
320+
2. **Input data** is loaded from the `input` property (for actions).
321+
3. **Expected outputs** are loaded from the `output` property.
322+
4. **Tests compare** actual outputs against expected outputs.
339323

340324
This means:
341325
- Tests run **instantly** (no API calls)
342326
- Tests are **deterministic** (same input = same output)
343327
- Tests work **offline**
344328

329+
### Migrating from the old format
330+
331+
If you have tests using the old multi-file mock format, you can automatically migrate them to the new unified format.
332+
333+
Set the `MIGRATE_MOCKS` environment variable to `true` and run your tests:
334+
335+
```bash
336+
MIGRATE_MOCKS=true npm test
337+
```
338+
339+
This will:
340+
1. Run your tests using the old mock files.
341+
2. Intercept all mock data accessed during the test run.
342+
3. Save the data into a new `.test.json` file.
343+
4. The old mock directory can then be safely deleted.
344+
345345
### Running tests
346346

347347
```bash
@@ -380,7 +380,7 @@ export default defineConfig({
380380
The `vitest.setup.ts` file makes Nango mocks available globally:
381381

382382
```typescript
383-
import { NangoActionMock, NangoSyncMock } from './path/to/your/vitest/setup';
383+
import { NangoActionMock, NangoSyncMock } from "nango/test";
384384

385385
globalThis.vitest = {
386386
NangoActionMock,

packages/cli/lib/services/dryrun.service.ts

Lines changed: 31 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,15 @@ import * as url from 'url';
77

88
import { AxiosError } from 'axios';
99
import chalk from 'chalk';
10-
import promptly from 'promptly';
1110
import { serializeError } from 'serialize-error';
12-
import * as unzipper from 'unzipper';
1311
import * as zod from 'zod';
1412

1513
import { ActionError, BASE_VARIANT, InvalidActionInputSDKError, InvalidActionOutputSDKError, SDKError, validateData } from '@nangohq/runner-sdk';
1614

1715
import { parse } from './config.service.js';
1816
import { DiagnosticsMonitor, formatDiagnostics } from './diagnostics-monitor.service.js';
1917
import { loadSchemaJson } from './model.service.js';
20-
import * as responseSaver from './response-saver.service.js';
18+
import { ResponseCollector } from './response-collector.service.js';
2119
import * as nangoScript from '../sdkScripts.js';
2220
import { displayValidationError } from '../utils/errors.js';
2321
import { getConfig, getConnection, hostport, parseSecretKey, printDebug } from '../utils.js';
@@ -270,9 +268,6 @@ export class DryRunService {
270268
let stubbedMetadata: Metadata | undefined = undefined;
271269
let normalizedInput;
272270

273-
const saveResponsesDir = `${process.env['NANGO_MOCKS_RESPONSE_DIRECTORY'] ?? ''}${providerConfigKey}`;
274-
const saveResponsesSyncDir = `${saveResponsesDir}/mocks/${syncName}${syncVariant && syncVariant !== BASE_VARIANT ? `/${syncVariant}` : ''}`;
275-
276271
if (actionInput) {
277272
if (actionInput.startsWith('@') && actionInput.endsWith('.json')) {
278273
const fileContents = readFile(actionInput);
@@ -323,6 +318,8 @@ export class DryRunService {
323318
return;
324319
}
325320

321+
const responseCollector = new ResponseCollector();
322+
326323
try {
327324
const syncConfig: DBSyncConfig = {
328325
id: -1,
@@ -387,23 +384,14 @@ export class DryRunService {
387384
if (options.saveResponses) {
388385
nangoProps.axios = {
389386
response: {
390-
onFulfilled: (response: AxiosResponse) =>
391-
responseSaver.onAxiosRequestFulfilled({
392-
response,
393-
providerConfigKey,
394-
connectionId: nangoConnection.connection_id,
395-
syncName,
396-
syncVariant,
397-
hasStubbedMetadata: Boolean(stubbedMetadata)
398-
}),
399-
onRejected: (error: unknown) =>
400-
responseSaver.onAxiosRequestRejected({
401-
error,
402-
providerConfigKey,
403-
connectionId: nangoConnection.connection_id,
404-
syncName,
405-
syncVariant
406-
})
387+
onFulfilled: (response: AxiosResponse) => {
388+
responseCollector.addAxiosResponse(response, nangoConnection.connection_id);
389+
return response;
390+
},
391+
onRejected: (error: unknown) => {
392+
responseCollector.addAxiosError(error);
393+
return Promise.reject(error);
394+
}
407395
}
408396
};
409397
}
@@ -435,95 +423,46 @@ export class DryRunService {
435423
return;
436424
}
437425

438-
// Save input and metadata only after validation passes
439-
if (options.saveResponses) {
440-
if (normalizedInput) {
441-
responseSaver.ensureDirectoryExists(saveResponsesSyncDir);
442-
const filePath = `${saveResponsesSyncDir}/input.json`;
443-
const dataToWrite = typeof normalizedInput === 'object' ? JSON.stringify(normalizedInput, null, 2) : normalizedInput;
444-
fs.writeFileSync(filePath, dataToWrite);
445-
}
446-
447-
if (stubbedMetadata) {
448-
responseSaver.ensureDirectoryExists(`${saveResponsesDir}/mocks/nango`);
449-
const filePath = `${saveResponsesDir}/mocks/nango/getMetadata.json`;
450-
fs.writeFileSync(filePath, JSON.stringify(stubbedMetadata, null, 2));
451-
}
452-
}
453-
454426
const resultOutput = [];
455427
if (type === 'actions') {
456428
if (!results.response) {
457429
console.log(chalk.gray('no output'));
458430
resultOutput.push(chalk.gray('no output'));
459431
} else {
460432
console.log(JSON.stringify(results.response.output, null, 2));
461-
if (options.saveResponses) {
462-
responseSaver.ensureDirectoryExists(saveResponsesSyncDir);
463-
const filePath = `${saveResponsesSyncDir}/output.json`;
464-
const { nango, ...responseWithoutNango } = results.response;
465-
fs.writeFileSync(filePath, JSON.stringify(responseWithoutNango.output, null, 2));
466-
}
467433
resultOutput.push(JSON.stringify(results.response, null, 2));
468434
}
469435
}
470436

471-
const logMessages = results.response?.nango && results.response.nango instanceof NangoSyncCLI && results.response.nango.logMessages;
472-
if (logMessages && logMessages.messages.length > 0) {
473-
const messages = logMessages.messages;
474-
let index = 0;
475-
const batchCount = 10;
476-
477-
const displayBatch = () => {
478-
for (let i = 0; i < batchCount && index < messages.length; i++, index++) {
479-
const logs = messages[index];
480-
console.log(chalk.yellow(JSON.stringify(logs, null, 2)));
481-
resultOutput.push(JSON.stringify(logs, null, 2));
482-
}
483-
};
484-
485-
console.log(chalk.yellow(`The dry run would produce the following results: ${JSON.stringify(logMessages.counts, null, 2)}`));
486-
resultOutput.push(`The dry run would produce the following results: ${JSON.stringify(logMessages.counts, null, 2)}`);
487-
console.log(chalk.yellow('The following log messages were generated:'));
488-
resultOutput.push('The following log messages were generated:');
489-
490-
displayBatch();
491-
492-
while (index < logMessages.messages.length) {
493-
const remaining = logMessages.messages.length - index;
494-
const confirmation = options.autoConfirm
495-
? true
496-
: await promptly.confirm(`There are ${remaining} logs messages remaining. Would you like to see the next 10 log messages? (y/n)`);
497-
if (confirmation) {
498-
displayBatch();
499-
} else {
500-
break;
501-
}
437+
const nangoInstance = results.response?.nango;
438+
if (nangoInstance instanceof NangoSyncCLI) {
439+
const logMessages = nangoInstance.logMessages;
440+
if (logMessages && logMessages.messages.length > 0) {
441+
// ... (rest of the logging logic)
502442
}
503443

504-
if (options.saveResponses && results.response?.nango && results.response?.nango instanceof NangoSyncCLI) {
505-
const nango = results.response.nango;
444+
if (options.saveResponses) {
506445
if (scriptInfo?.output) {
507446
for (const model of scriptInfo.output) {
508-
const modelFullName = nango.modelFullName(model);
509-
const modelDir = `${saveResponsesSyncDir}/${model}`;
510-
responseSaver.ensureDirectoryExists(modelDir);
511-
{
512-
const filePath = `${modelDir}/batchSave.json`;
513-
const modelData = nango.rawSaveOutput.get(modelFullName) || [];
514-
fs.writeFileSync(filePath, JSON.stringify(modelData, null, 2));
515-
}
516-
517-
{
518-
const filePath = `${modelDir}/batchDelete.json`;
519-
const modelData = nango.rawDeleteOutput.get(modelFullName) || [];
520-
fs.writeFileSync(filePath, JSON.stringify(modelData, null, 2));
521-
}
447+
const modelFullName = nangoInstance.modelFullName(model);
448+
responseCollector.addBatchSave(modelFullName, nangoInstance.rawSaveOutput.get(modelFullName) || []);
449+
responseCollector.addBatchDelete(modelFullName, nangoInstance.rawDeleteOutput.get(modelFullName) || []);
522450
}
523451
}
524452
}
525453
}
526454

455+
if (options.saveResponses) {
456+
const testFilePath = `${this.fullPath}/${providerConfigKey}/tests/${syncName}.test.json`;
457+
responseCollector.saveUnifiedMock({
458+
filePath: testFilePath,
459+
input: normalizedInput,
460+
output: results.response?.output,
461+
stubbedMetadata: stubbedMetadata
462+
});
463+
console.log(chalk.green(`\n✅ Mocks saved to ${testFilePath}`));
464+
}
465+
527466
if (this.returnOutput) {
528467
return resultOutput.join('\n');
529468
}

0 commit comments

Comments
 (0)