Skip to content

Conversation

@agusayerza
Copy link
Contributor

@agusayerza agusayerza commented Dec 19, 2025

This PR introduces a unified structure for test mocks, consolidating scattered JSON files into a single .test.json file per test suite.

Changes

Unified Mock Format

  • Updated nango dryrun --save to generate the new unified mock format
  • Implemented ResponseCollector to aggregate API calls during dry runs
  • Updated NangoActionMock and NangoSyncMock in nango/test to support the new format

Migration Path

  • Running MIGRATE_MOCKS=2026-01 npm test records legacy mock usage and generates the new unified file automatically
  • Migration captures all mock files for each endpoint (not just those accessed during the test run)
  • Handles both hash-based directory format and name-based file format

Pagination Bug Fix

The legacy test utilities had a bug where pagination would stop after the first page, causing:

  • Tests to falsely pass (only testing page 1)
  • Mock files with incorrect request hashes
  • Missing mock files for subsequent pages

Fixes implemented:

  • Fuzzy matching: When exact hash lookup fails, matches by comparing requestIdentity.params directly
  • Best-effort recovery: Migration recovers mocks where possible, with clear errors when not

Documentation

  • Updated testing docs to explain the pagination bug and migration behavior
  • Clear error messages guide users to re-record with nango dryrun <sync> <connection> --save

Note: Follow-up needed in integration-templates repo to update vitest.setup.ts to import from nango/test instead of using local mock implementations.

NAN-4463

Testing Instructions

  1. Run nango dryrun <name> <connectionId> --save and verify a single .test.json is created
  2. Run npm test to verify tests pass with the new format
  3. (Optional) In a project with legacy mocks, run MIGRATE_MOCKS=2026-01 npm test and verify the new mock file is generated
  4. (Optional) Test pagination: verify paginated endpoints capture all pages in the migrated file

The unified fixtures capture complete request and response metadata—including connection context and proxied traffic—so the CLI helpers can replay dry runs, manage legacy-to-unified migration inline, and actively close the long-standing pagination gap.

Affected Areas

packages/cli/lib/services/response-collector.service.ts
packages/cli/lib/services/dryrun.service.ts
packages/cli/lib/testMocks/utils.ts
docs/implementation-guides/building-integrations/testing.mdx
packages/cli/package.json


This summary was automatically generated by @propel-code-bot

@linear
Copy link

linear bot commented Dec 19, 2025

@my-senior-dev-pr-review
Copy link

my-senior-dev-pr-review bot commented Dec 19, 2025

🤖 My Senior Dev — Analysis Complete

👤 For @agusayerza

📁 Expert in packages/ (20 edits) • ⚡ 2nd PR this month

View your contributor analytics →


📊 7 files reviewed • 1 high risk • 5 need attention

🚨 High Risk:

  • packages/cli/lib/services/response-collector.service.ts — Newly introduced service critically handles user inputs and sensitive data during API interactions.

⚠️ Needs Attention:

  • packages/cli/lib/services/dryrun.service.ts — Changes in this service impact API response handling, crucial for functionality.
  • docs/implementation-guides/building-integrations/testing.mdx — Significant documentation changes require careful review to avoid user confusion during migration.
  • +1 more concerns...

🚀 Open Interactive Review →

The full interface unlocks features not available in GitHub:

  • 💬 AI Chat — Ask questions on any file, get context-aware answers
  • 🔍 Smart Hovers — See symbol definitions and usage without leaving the diff
  • 📚 Code Archeology — Understand how files evolved over time (/archeology)
  • 🎯 Learning Insights — See how this PR compares to similar changes

💬 Chat here: @my-senior-dev explain this change — or try @chaos-monkey @security-auditor @optimizer @skeptic @junior-dev

📖 View all 12 personas & slash commands

You can interact with me by mentioning @my-senior-dev in any comment:

In PR comments or on any line of code:

  • Ask questions about the code or PR
  • Request explanations of specific changes
  • Get suggestions for improvements

Slash commands:

  • /help — Show all available commands
  • /archeology — See the history and evolution of changed files
  • /profile — Performance analysis and suggestions
  • /expertise — Find who knows this code best
  • /personas — List all available AI personas

AI Personas (mention to get their perspective):

Persona Focus
@chaos-monkey 🐵 Edge cases & failure scenarios
@skeptic 🤨 Challenge assumptions
@optimizer Performance & efficiency
@security-auditor 🔒 Security vulnerabilities
@accessibility-advocate Inclusive design
@junior-dev 🌱 Simple explanations
@tech-debt-collector 💳 Code quality & shortcuts
@ux-champion 🎨 User experience
@devops-engineer 🚀 Deployment & scaling
@documentation-nazi 📚 Documentation gaps
@legacy-whisperer 🏛️ Working with existing code
@test-driven-purist Testing & TDD

For the best experience, view this PR on myseniordev.com — includes AI chat, file annotations, and interactive reviews.

@agusayerza agusayerza force-pushed the agus/NAN-4463/mock-structure-refactor/update-docs branch from 024943e to 07a1f7c Compare December 19, 2025 15:21
@agusayerza agusayerza force-pushed the agus/NAN-4463/mock-structure-refactor/update-docs branch from 07a1f7c to 6cead33 Compare December 19, 2025 18:42
@agusayerza agusayerza force-pushed the agus/NAN-4463/mock-structure-refactor/update-docs branch from 6cead33 to 70da92c Compare December 19, 2025 18:50
@agusayerza agusayerza force-pushed the agus/NAN-4463/mock-structure-refactor/update-docs branch from 70da92c to a9b81ed Compare December 22, 2025 14:56
@agusayerza agusayerza force-pushed the agus/NAN-4463/mock-structure-refactor/update-docs branch from a9b81ed to dec42a9 Compare December 22, 2025 17:32
@agusayerza agusayerza force-pushed the agus/NAN-4463/mock-structure-refactor/update-docs branch from dec42a9 to f3ac9c1 Compare December 22, 2025 17:46
@agusayerza agusayerza requested a review from a team December 23, 2025 13:17
@agusayerza agusayerza marked this pull request as ready for review December 23, 2025 13:17

let requestData = call.requestIdentity.data;
if (typeof requestData === 'string') {
requestData = JSON.parse(requestData);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can the parsing throw?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might, if the request isn't json. I have wrapped it with a try/catch

import { parse } from './config.service.js';
import { DiagnosticsMonitor, formatDiagnostics } from './diagnostics-monitor.service.js';
import { loadSchemaJson } from './model.service.js';
import * as responseSaver from './response-saver.service.js';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can the 'response-saver.service.ts` file be deleted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}

private computeRequestIdentity(config: AxiosRequestConfig): { requestIdentity: RequestIdentity; requestIdentityHash: string } {
const method = config.method?.toUpperCase() || 'GET';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

previous logic was using lowercase. Isn't using uppercase changing the identity hash and making it incompatible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, although it wouldn't really matter as this is for new test generations, and when parsing the request at test runtime, we still LowerCase it. Regardless, I changed it back to use lowercase as there is no reason to modify our behaviour.

ptrtht added a commit to ptrtht/nango that referenced this pull request Jan 5, 2026
Updated the action implementation guide to fix a typo in the code example, changing '<ATION-NAME>' to '<ACTION-NAME>''
github-merge-queue bot pushed a commit that referenced this pull request Jan 6, 2026
Updated the action implementation guide to fix a typo in the code
example, changing '<ATION-NAME>' to '<ACTION-NAME>''

It's just a typo in the docs that was bugging me for days.
@agusayerza agusayerza force-pushed the agus/NAN-4463/mock-structure-refactor/update-docs branch from f3ac9c1 to 97b8043 Compare January 12, 2026 16:37
@agusayerza agusayerza force-pushed the agus/NAN-4463/mock-structure-refactor/update-docs branch from 97b8043 to be82deb Compare January 12, 2026 17:02
hassan254-prog pushed a commit that referenced this pull request Jan 12, 2026
Updated the action implementation guide to fix a typo in the code
example, changing '<ATION-NAME>' to '<ACTION-NAME>''

It's just a typo in the docs that was bugging me for days.
@agusayerza agusayerza force-pushed the agus/NAN-4463/mock-structure-refactor/update-docs branch from be82deb to 4a75028 Compare January 13, 2026 15:55
@agusayerza agusayerza requested a review from TBonnin January 13, 2026 15:57
@agusayerza agusayerza force-pushed the agus/NAN-4463/mock-structure-refactor/update-docs branch from 4a75028 to 8e60791 Compare January 16, 2026 17:42
Copy link
Collaborator

@TBonnin TBonnin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you tested the performance with a huge test.json file?


public onAxiosRequestFulfilled(response: AxiosResponse, connectionId: string): AxiosResponse {
// Handle getConnection/getMetadata calls
if (response.request.path.includes(`/connections/${connectionId}`)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the string doesn't match the comment.

try {
requestData = JSON.parse(requestData);
} catch {
// ignore
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we output a warning here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a warning, this shouldn't happen but better to be safe than sorry

if (nangoInstance instanceof NangoSyncCLI) {
const logMessages = nangoInstance.logMessages;
if (logMessages && logMessages.messages.length > 0) {
// ... (rest of the logging logic)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that a TODO?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI doing AI stuff. Good catch


import { AxiosError } from 'axios';
import chalk from 'chalk';
import promptly from 'promptly';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is promptly used anywhere else? if not let's remove it from package.json

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is still being used

}
}

throw new Error(`No mock found for ${method.toUpperCase()} ${endpoint} (normalized: ${normalizedEndpoint}) with hash ${requestIdentityHash}`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like we can simplify this piece of code with some early return and utility function.

const apiMock = this.mockData.api?.[method.toUpperCase()]?.[normalizedEndpoint] 
            || this.mockData.api?.[method.toUpperCase()]?.[`/${normalizedEndpoint}`];
if (!apiMock) { throw ... } ;

if (!Array.isArray(apiMock)) return apiMock;

const getFieldsMatch = (expected, actual, opts: { caseSensitive: bool } = {caseSensitive: true}) {
  return Object.entries(expected).every(([key, value]) => {
    const actualField = actual.find(([k]) => 
      opts.caseSensitive ? k.toLowerCase() === key.toLowerCase() : k === key
    );
    return actualField && String(actualField[1]) === String(value);
  });
}

// algo: hash > params > headers > data
const matchedMock = apiMock.find((mock) => {

  if (mock.hash === requestIdentityHash) return true;
  
  if (!mock.request) return false;
  
  if (mock.request.params && 
      !fieldsMatch(mock.request.params, identity.requestIdentity.params, { caseSensitive: false})) {
    return false;
  }
  
  if (mock.request.headers && 
      !fieldsMatch(mock.request.headers, identity.requestIdentity.headers)) {
    return false;
  }
  
  if (mock.request.data !== undefined) {
    const expectedDataIdentity = computeDataIdentity({ data: mock.request.data });
    if (expectedDataIdentity !== identity.requestIdentity.data) {
      return false;
    }
  }
  
  return true;
});

return matchedMock;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Set the `MIGRATE_MOCKS` environment variable to `true` and run your tests:

```bash
MIGRATE_MOCKS=true npm test
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about something like MOCKS_MIGRATION=2026-01 or similar. In case we need to migrate again in the future :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, much better

}
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why cannot we re-used the pagination logic?


private getNextPageLinkFromBodyOrHeaders(linkPagination: LinkPagination, response: MockResponse, paginationConfig: Pagination) {
if (linkPagination.link_rel_in_response_header) {
const linkHeader = parseLinksHeader(response.headers['link']);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need an extra dependency for parsing the link header?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its already being used in the runners, I think it makes sense to use the same dependency to keep it consistent. This bugged pagination logic is slated to be removed once we migrate all tests on integration-templates

1. Run your tests using the old mock files.
2. Intercept all mock data accessed during the test run.
3. Save the data into a new `.test.json` file.
4. The old mock directory can then be safely deleted.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we name the old mock directory to make it super clear what can be deleted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@agusayerza agusayerza force-pushed the agus/NAN-4463/mock-structure-refactor/update-docs branch from 9ed643c to dadeda5 Compare January 22, 2026 17:46
@agusayerza agusayerza force-pushed the agus/NAN-4463/mock-structure-refactor/update-docs branch from dadeda5 to f4c6207 Compare January 23, 2026 12:13
@agusayerza agusayerza requested review from a team and TBonnin January 23, 2026 12:25
@agusayerza
Copy link
Contributor Author

have you tested the performance with a huge test.json file?

Yes, actually I tested it on the whole integration-templates repo. I don't think there are HUGE files, but its the most real use case scenario possible. It wasn't a timed test, but for the most part runs last roughly the same.

Copy link
Collaborator

@TBonnin TBonnin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @agusayerza

@agusayerza agusayerza added this pull request to the merge queue Jan 26, 2026
Merged via the queue into master with commit ff200dd Jan 26, 2026
24 checks passed
@agusayerza agusayerza deleted the agus/NAN-4463/mock-structure-refactor/update-docs branch January 26, 2026 12:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants