Skip to content

Commit a9b81ed

Browse files
committed
Update dryrun
1 parent f43d82c commit a9b81ed

File tree

4 files changed

+845
-184
lines changed

4 files changed

+845
-184
lines changed

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

Lines changed: 27 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,36 @@ 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+
345+
> **Note:** This migration tool works by intercepting the mock data loaded by your existing tests. It requires that your tests are using the standard Nango mock utilities (`NangoSyncMock` or `NangoActionMock`) imported from `nango/test`.
346+
345347
### Running tests
346348

347349
```bash
@@ -380,7 +382,7 @@ export default defineConfig({
380382
The `vitest.setup.ts` file makes Nango mocks available globally:
381383

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

385387
globalThis.vitest = {
386388
NangoActionMock,

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

Lines changed: 25 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ 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';
1211
import * as unzipper from 'unzipper';
1312
import * as zod from 'zod';
@@ -17,7 +16,7 @@ import { ActionError, BASE_VARIANT, InvalidActionInputSDKError, InvalidActionOut
1716
import { parse } from './config.service.js';
1817
import { DiagnosticsMonitor, formatDiagnostics } from './diagnostics-monitor.service.js';
1918
import { loadSchemaJson } from './model.service.js';
20-
import * as responseSaver from './response-saver.service.js';
19+
import { ResponseCollector } from './response-collector.service.js';
2120
import * as nangoScript from '../sdkScripts.js';
2221
import { displayValidationError } from '../utils/errors.js';
2322
import { getConfig, getConnection, hostport, parseSecretKey, printDebug } from '../utils.js';
@@ -270,9 +269,6 @@ export class DryRunService {
270269
let stubbedMetadata: Metadata | undefined = undefined;
271270
let normalizedInput;
272271

273-
const saveResponsesDir = `${process.env['NANGO_MOCKS_RESPONSE_DIRECTORY'] ?? ''}${providerConfigKey}`;
274-
const saveResponsesSyncDir = `${saveResponsesDir}/mocks/${syncName}${syncVariant && syncVariant !== BASE_VARIANT ? `/${syncVariant}` : ''}`;
275-
276272
if (actionInput) {
277273
if (actionInput.startsWith('@') && actionInput.endsWith('.json')) {
278274
const fileContents = readFile(actionInput);
@@ -323,6 +319,8 @@ export class DryRunService {
323319
return;
324320
}
325321

322+
const responseCollector = new ResponseCollector();
323+
326324
try {
327325
const syncConfig: DBSyncConfig = {
328326
id: -1,
@@ -387,23 +385,8 @@ export class DryRunService {
387385
if (options.saveResponses) {
388386
nangoProps.axios = {
389387
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-
})
388+
onFulfilled: (response: AxiosResponse) => responseCollector.onAxiosRequestFulfilled(response, nangoConnection.connection_id),
389+
onRejected: (error: unknown) => responseCollector.onAxiosRequestRejected(error)
407390
}
408391
};
409392
}
@@ -435,95 +418,46 @@ export class DryRunService {
435418
return;
436419
}
437420

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-
454421
const resultOutput = [];
455422
if (type === 'actions') {
456423
if (!results.response) {
457424
console.log(chalk.gray('no output'));
458425
resultOutput.push(chalk.gray('no output'));
459426
} else {
460427
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-
}
467428
resultOutput.push(JSON.stringify(results.response, null, 2));
468429
}
469430
}
470431

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-
}
432+
const nangoInstance = results.response?.nango;
433+
if (nangoInstance instanceof NangoSyncCLI) {
434+
const logMessages = nangoInstance.logMessages;
435+
if (logMessages && logMessages.messages.length > 0) {
436+
// ... (rest of the logging logic)
502437
}
503438

504-
if (options.saveResponses && results.response?.nango && results.response?.nango instanceof NangoSyncCLI) {
505-
const nango = results.response.nango;
439+
if (options.saveResponses) {
506440
if (scriptInfo?.output) {
507441
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-
}
442+
const modelFullName = nangoInstance.modelFullName(model);
443+
responseCollector.addBatchSave(modelFullName, nangoInstance.rawSaveOutput.get(modelFullName) || []);
444+
responseCollector.addBatchDelete(modelFullName, nangoInstance.rawDeleteOutput.get(modelFullName) || []);
522445
}
523446
}
524447
}
525448
}
526449

450+
if (options.saveResponses) {
451+
const testFilePath = `${this.fullPath}/${providerConfigKey}/tests/${syncName}.test.json`;
452+
responseCollector.saveUnifiedMock({
453+
filePath: testFilePath,
454+
input: normalizedInput,
455+
output: results.response?.output,
456+
stubbedMetadata: stubbedMetadata
457+
});
458+
console.log(chalk.green(`\n✅ Mocks saved to ${testFilePath}`));
459+
}
460+
527461
if (this.returnOutput) {
528462
return resultOutput.join('\n');
529463
}

0 commit comments

Comments
 (0)