Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add mock call history to access request configuration in test #4029

Open
wants to merge 24 commits into
base: main
Choose a base branch
from

Conversation

blephy
Copy link

@blephy blephy commented Jan 25, 2025

This relates to...

#4025

Rationale

Actually we are not able with MockAgent to assert request configuration.

Given this in mind, the only way we could do it is by wrapping our app http call in function, and then spy or mock that function with our favorite test framework. We can make assertions on parameters we passed through that function but, even with this possibility, we are not certain that undici did not make computation on inputs.

This PR is addressing this problematic : ensuring http calls are made with the expected low level configuration, as far as we can.

Changes

  • add MockCallHistory and MockCallHistoryLog class
  • add getCallHistory class method to MockAgent instance
  • add clearCallHistory class method to MockAgent instance
  • add disableCallHistory class method to MockAgent instance
  • add enableCallHistory class method to MockAgent instance
  • add enableCallHistory MockAgent instantiation options
  • clear all call history logs when MockAgent.close is called

Features

Given this application :

function myCall(body: Record<string, unknown>, token: string) {
  await fetch(`https://my-service.io/endpoint`, {
      method: 'POST',
      body: JSON.stringify(body),
      headers: {
        authorization: 'Bearer ${token}',
        'content-type': 'application/json'
      }
  })
}

async function startApp(): Promise<void> {
  await myCall({ data: 'hello' }, 'myToken')
}

After :

describe('my app', () => {
  let agent: MockAgent

  before(() => {
    agent = new MockAgent({ enableCallHistory: true })
    setGlobalDispatcher(agent)
  })

  beforeEach(() => {
    agent.clearCallHistory()
  })

  after(async () => {
    await agent.close()
  })

  it('should request my external service with a specific body and headers', async () => {
    mockAgent.get('https://my-service.io').intercept({ path: '/endpoint', method: 'POST' }).reply(201, 'OK').times(1)

    await startApp()

    expect(mockAgent.getCallHistory()?.firstCall()?.body).toStrictEqual('"{ "data": "hello" }"')
    expect(mockAgent.getCallHistory()?.firstCall()?.headers['authorization']).toStrictEqual('Bearer myToken')
    expect(mockAgent.getCallHistory()?.firstCall()?.headers['content-type']).toStrictEqual('application/json')
    expect(mockAgent.getCallHistory()?.firstCall()?.path).toStrictEqual('/endpoint')
    expect(mockAgent.getCallHistory()?.firstCall()?.protocol).toStrictEqual('https:')
    expect(mockAgent.getCallHistory()?.calls()?.length).toStrictEqual(1)
  })

  it('should always call external services with a secure HTTP protocol', async () => {
    await startApp()

    // working without intercepting too

    expect(mockAgent.getCallHistory()?.filterCalls({ protocol: /^((?!https).)*$/ })?.length).toStrictEqual(0)
  })
})

Before :

describe('my app', () => {
  let agent: MockAgent

  before(() => {
    agent = new MockAgent({ enableCallHistory: true })
    setGlobalDispatcher(agent)
  })

  beforeEach(() => {
    agent.clearCallHistory()
  })

  after(async () => {
    await agent.close()
  })

  it('should request my external service with a specific body and headers', async () => {
    const spy = spyOn(myCall)
    mockAgent.get('https://my-service.io').intercept({ path: '/endpoint', method: 'POST' }).reply(201, 'OK').times(1)

    await startApp()

    expect(spy.calls?.[0]).toHaveBeenCalledWith({ data: 'hello' }, 'myToken')
    // we cannot do it, we must modify our application code and this will only assert function parameters, not the real http request
    // expect(mockAgent.getCallHistory()?.firstCall()?.headers['authorization']).toMatch(/^Bearer /)
    // we cannot do it, we must modify our application code and this will only assert function parameters, not the real http request
    // expect(mockAgent.getCallHistory()?.firstCall()?.headers['content-type']).toStrictEqual('application/json')
    // we cannot do it, we must modify our application code and this will only assert function parameters, not the real http request
    // expect(mockAgent.getCallHistory()?.firstCall()?.path).toStrictEqual('/endpoint')
    // we cannot do it, we must modify our application code and this will only assert function parameters, not the real http request
    // expect(mockAgent.getCallHistory()?.firstCall()?.protocol).toStrictEqual('https:')
    expect(spy.calls.length).toStrictEqual(1)
  })

  it('should always call external services with a secure HTTP protocol', async () => {
    await startApp()

    // we cannot do it, we must modify our application code and this will only assert function parameters, not the real http request
    // expect(mockAgent.getCallHistory()?.filterCalls({ protocol: /^((?!https).)*$/ })?.length).toStrictEqual(0)
  })
})

Status

@metcoder95 metcoder95 linked an issue Jan 26, 2025 that may be closed by this pull request
Copy link
Member

@metcoder95 metcoder95 left a comment

Choose a reason for hiding this comment

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

Can you add testing for it?

@blephy
Copy link
Author

blephy commented Jan 26, 2025

Yes for sur. I just need times

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

Thanks for opening a PR! Can you please add a unit test?

@blephy blephy requested review from mcollina and metcoder95 January 27, 2025 00:28
@blephy blephy changed the title feat: add mock call histories feat: add mock call history Jan 27, 2025
@blephy blephy changed the title feat: add mock call history feat: add mock call history to access request configuration in test Jan 27, 2025
const body = { data: 'value' }
const query = { a: 1 }
url.search = new URLSearchParams(query)
const headers = { authorization: 'Bearer token', 'content-type': 'application/json' }

Check failure

Code scanning / CodeQL

Hard-coded credentials

The hard-coded value "Bearer token" is used as [authorization header](1).
@blephy blephy requested a review from metcoder95 January 28, 2025 19:26
Copy link
Member

@metcoder95 metcoder95 left a comment

Choose a reason for hiding this comment

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

lgtm, small comment left

@blephy blephy requested a review from metcoder95 January 29, 2025 18:20
Copy link
Member

@metcoder95 metcoder95 left a comment

Choose a reason for hiding this comment

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

LGTM, left minor comment and should be ready on my end

@blephy blephy requested a review from metcoder95 January 30, 2025 11:27
@blephy
Copy link
Author

blephy commented Jan 31, 2025

@mcollina can you take a bit of time to review back this PR ? it would be nice to have this change in the next release 🙏🏼

@blephy
Copy link
Author

blephy commented Feb 7, 2025

@mcollina 😢

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

Thanks for doing this.

I'm also sorry it took so long for me to check out. I've been buried in work for the last few weeks.

I have question: why is the call history log a singleton? I don't think it should be.

@blephy
Copy link
Author

blephy commented Feb 17, 2025

Hi @mcollina !

I saw that, don't worry about, i looked at your work and maintaining all of this open source projects + a job is not an easy part ;)

To answer at your question :

  • MockCallHistoryLog is not a singleton. it contains information about one request configuration and is instantiable.
  • MockCallHistory is an hybride singleton. it contains every mock call history log for a particular set of requests. You can instantiate a MockCallHistory but at instantiation the instance is registered on a private static class method to be able to :
    • clear every instante when needed
    • delete every instante when needed
    • retrieve an instance based on the name of the set of requests.

NB : the main purpose of the static class method is to avoid to register and associate an history instance to a MockAgent or a MockScope. In my own opinion, this is the most simple way to do it and the safer way to register multiple instance when constructing multiple MockAgent / MockScope withou having to forget to delete them.

@mcollina
Copy link
Member

NB : the main purpose of the static class method is to avoid to register and associate an history instance to a MockAgent or a MockScope. In my own opinion, this is the most simple way to do it and the safer way to register multiple instance when constructing multiple MockAgent / MockScope withou having to forget to delete them.

Can you please remove it, and have it passed directly to MockAgent, or just create one in MockAgent if enabled? We already have one singleton (the default dispatcher), I don't want to have two to reset after each test.

@blephy
Copy link
Author

blephy commented Feb 17, 2025

Ok i will rollback to my previous implementation, but delete the ability to bind a mockCallHistory instance to a mockDispatch because without this static class method, i cannot get the mockCallHistory associated with a mockDispatch with mockAgent.getCallHistory(name) :

  • attach a MockCallHistory instance to a MockAgent instance if / when enabled
  • when mockAgent.close is called, the MockCallHistory instance associated with the mockAgent instance will be cleared (the option / instance will still be set for reuse)

WDYT @mcollina ?

ps : i can do it this night

@mcollina
Copy link
Member

that should work

@blephy
Copy link
Author

blephy commented Feb 18, 2025

@mcollina this is done in this commit 0f6f62e

I also rebase on main and force pushed

@blephy blephy requested a review from mcollina February 18, 2025 20:35
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.

Mock history
3 participants