jfairbank / redux-saga-test-plan

Test Redux Saga with an easy plan.
http://redux-saga-test-plan.jeremyfairbank.com
MIT License
1.25k stars 127 forks source link

Is it possible to use dynamic providers with race? #325

Open HorizonXP opened 4 years ago

HorizonXP commented 4 years ago

I just spent the last few days trying to debug an issue with my tests, and I think I've figured out what part of the problem was. I currently have a hack to workaround this, so it's not critical.

I was trying to test the login flow for my application, which looks like the following:

export function* loginFlow() {
  let accessToken = JSON.parse(
    yield call([localStorage, 'getItem'], 'accessToken')
  )
  let refreshToken = JSON.parse(
    yield call([localStorage, 'getItem'], 'refreshToken')
  )
  let user = JSON.parse(yield call([localStorage, 'getItem'], 'user'))
  while (true) {
    if (!refreshToken) {
      const {
        payload: { email, password },
      } = yield take(LOGIN)
      user = { email }
      ;({ refreshToken, accessToken } = yield call(authorize, email, password))
    }
    yield race({
      authLoop: call(authorizeLoop, accessToken, refreshToken, user),
      logout: take(LOGOUT),
    })
    refreshToken = null
    accessToken = null
    yield call([localStorage, 'removeItem'], 'accessToken')
    yield call([localStorage, 'removeItem'], 'refreshToken')
    yield call([localStorage, 'removeItem'], 'user')
    yield put(clearAuthentication())
  }
}

function* authorizeLoop(
  accessToken: AccessToken,
  refreshToken: RefreshToken,
  user
) {
  while (true) {
    if (!accessToken || !refreshToken || !user) {
      return
    }
    yield put(setAccessToken(accessToken))
    yield put(setRefreshToken(refreshToken))

    yield call(
      [localStorage, 'setItem'],
      'accessToken',
      JSON.stringify(accessToken)
    )
    yield call(
      [localStorage, 'setItem'],
      'refreshToken',
      JSON.stringify(refreshToken)
    )
    yield call([localStorage, 'setItem'], 'user', JSON.stringify(user))
    yield put(loginSuccess(user.email))
    const now = Date.now()
    const accessTokenDelay = Math.max(
      Math.min(Math.floor((accessToken.exp * 1000 - now) / 2), 0x7fffffff),
      0
    )
    const refreshTokenDelay = Math.max(
      Math.min(Math.floor(refreshToken.exp * 1000 - now), 0x7fffffff),
      0
    )
    const { accessTokenExpired, refreshTokenExpired } = yield race({
      accessTokenExpired: delay(accessTokenDelay),
      refreshTokenExpired: delay(refreshTokenDelay),
    })
    if (accessTokenExpired) {
      accessToken = yield call(api.auth.refreshAccessToken, refreshToken)
    } else if (refreshTokenExpired) {
      yield put(logout())
    }
  }
}

Ideally, I would have used expectSaga(loginFlow).provide([...]) with the following provider:

let loopCount = 0
const { effects } = await expectSaga(loginFlow)
        .provide([
            race: (args, next) => {
              if (
                'accessTokenExpired' in args &&
                'refreshTokenExpired' in args
              ) {
                if (loopCount === 0) {
                  loopCount++
                  return {
                    accessTokenExpired: true,
                    refreshTokenExpired: false,
                  }
                } else {
                  return {
                    accessTokenExpired: false,
                    refreshTokenExpired: true,
                  }
                }
              }
              return next()
            },
          },
        ])

But the problem arises with the first race() call. It seems that what happens is that my provider gets called for the first race({ authLoop, logout }), and then my provider calls next(), which seems to execute it in-place, skipping subsequent provider checks. I know this because I had a matchers.call.fn(api.auth.refreshAccessToken) that wasn't being called when it should have been.

For now, my workaround has been to match the delay() calls and try to differentiate them based on the arguments being passed.

My question is, is my return next() call incorrect? Or is this a bug?