lundi 11 décembre 2017

Jest test with mocked head request expects promise but receives number

I'm working on a small app that parses tweets and replaces shortened URLs with expanded ones. And I'm working on a utility function that handles the edge cases where t.co links resolve to another shortened url like bit.ly by making a small head request and checking whether is a location field in the response. I'm using a helper class to create Nock mocks to return sample data for interfacing with dummy URLs and APIs. When I test the utility function in isolation it works fine, but when used as a part of some sort of text replacement it acts wacky. I'm testing this utility function in tandem with other functions several times in my test file, and it generally only occurs on the first test. The following error comes up.

received value must be a Promise.
    Received:
      number: 591

      at Object.<anonymous> (node_modules/expect/build/index.js:119:11)
          at Generator.next (<anonymous>)
      at step (node_modules/expect/build/index.js:40:727)
      at node_modules/expect/build/index.js:40:957
          at new Promise (<anonymous>)
      at Object.toEqual (node_modules/expect/build/index.js:40:638)
      at Object.test (test/twitter.test.js:52:40)
          at <anonymous>
      at process._tickCallback (internal/process/next_tick.js:188:7)

Code below. Probably offering too much but erring on side of caution.

The actual test

   test('Replaces non-media t.co link of another shortened url with actual url', async () => {
      await expect(Tweet.replaceUrls(tweetData[7])).resolves.toBe('Tweet with shortened link. http://ift.tt/2Bc2YMt')
    })

Utility function (rp is request-promise)

export const getActualUrl = async (uri) => {
  try {
    return (await rp({
      method: 'HEAD',
      simple: false,
      followRedirect: false,
      followOriginalHttpMethod: true,
      uri,
    })).location || uri
  } catch (e) {
    return uri
  }
}

Text replacement class method used to swap out links. As seen below, I've tried both the string-replace-async module and the more cumbersome method of iterating through matches via the string match property and performing replace on the initial text for each match.

static async replaceUrls(data) {
    const matches = data.full_text.match(/(\bhttps\:\/\/t\.co\/\w+\b)/gi)

    if (!matches || !matches.length) {
      return data.full_text
    }

    let newText = data.full_text
    for (let match of matches) {
      let newUrl
      const nonMediaUrl = (data.entities.urls.find(item => item.url === match) || {}).expanded_url
      if (!nonMediaUrl) {
        if (!_.has(data, 'extended_entities.media')) newUrl = match

        const mediaUrls = data.extended_entities.media.filter(item => item.url === match)
        if (!mediaUrls.length) newUrl = match
        newUrl = mediaUrls.map((item) => {
          if (item.type === 'photo') return item.media_url
          return `${item.media_url} ${_.minBy(item.video_info.variants, 'bitrate').url}`
        }).join(' ')
      } else if (!nonMediaUrl.includes('facebook.com/') && /\.\w{1,4}\/\w+$/.test(nonMediaUrl)) {
        newUrl = await getActualUrl(nonMediaUrl)
      } else {
        newUrl = nonMediaUrl
      }
      newText = newText.replace(match, newUrl)
    }

    return newText

    // return stringAsyncReplace(data.full_text, /(\bhttps\:\/\/t\.co\/\w+\b)/gi, async (match) => {
    //   const nonMediaUrl = (data.entities.urls.find(item => item.url === match) || {}).expanded_url
    //   if (!nonMediaUrl) {
    //     if (!_.has(data, 'extended_entities.media')) return match

    //     const mediaUrls = data.extended_entities.media.filter(item => item.url === match)
    //     if (!mediaUrls.length) return match
    //     return mediaUrls.map((item) => {
    //       if (item.type === 'photo') return item.media_url
    //       return `${item.media_url} ${_.minBy(item.video_info.variants, 'bitrate').url}`
    //     }).join(' ')
    //   } else if (!nonMediaUrl.includes('facebook.com/') && /\.\w{1,4}\/\w+$/.test(nonMediaUrl)) {
    //     return getActualUrl(nonMediaUrl)
    //   }
    //   return nonMediaUrl
    // })
  }

Nocks from the API helper init method and cleanup method for resetting mocks:

static cleanMocks() {
    return nock.cleanAll()
  }
init() {
    this.constructor.cleanMocks()

    nock(/twitter\.com/)
      .persist()
      // .log(console.log)

      .get(/.*/)
      .reply(this.handleApiReply('twitter', 'GET'))
      .post(/.*/)
      .reply(this.handleApiReply('twitter', 'POST'))

    nock(/github\.com/)
      .persist()
      .get(/.*/)
      .reply(this.handleApiReply('github', 'GET'))
      .post(/.*/)
      .reply(this.handleApiReply('github', 'POST'))
      .patch(/.*/)
      .reply(this.handleApiReply('github', 'POST'))

    nock(/githubusercontent\.com/)
      .persist()
      .get(/.*/)
      .reply(this.handleApiReply('githubContent', 'GET'))

    nock(/testurl\.com/)
      .persist()
            // .log(console.log)

      .head('/sh0rt')
      .reply(301, {
        location: 'http://ift.tt/2Bc2YMt',
      }, {
        location: 'http://ift.tt/2Bc2YMt',
      })
      .head('/normal')
      .reply(200)
  }

beforeAll/afterAll et al for setting up tests in test file:

jasmine.DEFAULT_TIMEOUT_INTERVAL = 6000

const data = {}
let mockApi

const loadData = () => {
  data.time = {
    todayDate: '2017-02-02',
  }
  data.lastRun = modifyDate(data.time.todayDate, -1, 'hour')
  data.accounts = extractAccounts(JSON.parse(fs.readFileSync(path.join(__dirname, '/data/users.json'))))
}

beforeAll(() => {
  loadData()
  mockApi = new MockApi()
  mockApi.init()
})

afterAll(() => {
  MockApi.cleanMocks()
  jest.resetModules()
})

I'm using Node v9, yarn 1.3.2, Jest v21, babel-jest v21.

Aucun commentaire:

Enregistrer un commentaire