Skip to content

Commit

Permalink
test(client): stateTime + refetchInterval
Browse files Browse the repository at this point in the history
  • Loading branch information
ubbe-xyz committed Jul 18, 2021
1 parent 6188f2b commit 34ffda9
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 44 deletions.
88 changes: 63 additions & 25 deletions src/client/__tests__/client-provider.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState } from "react"
import { rest } from "msw"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSession } from "./helpers/mocks"
import { printFetchCalls } from "./helpers/utils"
import { SessionProvider, useSession, signOut } from "../react"
import { SessionProvider, useSession, signOut, getSession } from "../react"

const origDocumentVisibility = document.visibilityState
const fetchSpy = jest.spyOn(global, "fetch")
Expand All @@ -25,8 +26,8 @@ test("fetches the session once and re-uses it for different consumers", async ()

render(<ProviderFlow />)

expect(screen.getByTestId("session-consumer-1")).toHaveTextContent("loading")
expect(screen.getByTestId("session-consumer-2")).toHaveTextContent("loading")
expect(screen.getByTestId("session-1")).toHaveTextContent("loading")
expect(screen.getByTestId("session-2")).toHaveTextContent("loading")

return waitFor(() => {
expect(fetchSpy).toHaveBeenCalledTimes(1)
Expand All @@ -36,8 +37,8 @@ test("fetches the session once and re-uses it for different consumers", async ()
expect.anything()
)

const session1 = screen.getByTestId("session-consumer-1").textContent
const session2 = screen.getByTestId("session-consumer-2").textContent
const session1 = screen.getByTestId("session-1").textContent
const session2 = screen.getByTestId("session-2").textContent

expect(session1).toEqual(session2)
})
Expand All @@ -46,11 +47,6 @@ test("fetches the session once and re-uses it for different consumers", async ()
test("when there's an existing session, it won't try to fetch a new one straightaway", async () => {
fetchSpy.mockClear()

/**
* TODO: How can we force a clean session state between tests? At the moment
* the library uses this internal constant: __NEXTAUTH to track calls
* and we can't clean it between tests.
*/
render(<ProviderFlow session={mockSession} />)

expect(fetchSpy).not.toHaveBeenCalled()
Expand Down Expand Up @@ -106,7 +102,6 @@ test("will refetch the session if told to do so programmatically from another wi
)

// We should have a call to sign-out and a call to refetch the session accordingly
// TODO: investigate why the CSRF endpoint is called.
expect(printFetchCalls(fetchSpy.mock.calls)).toMatchInlineSnapshot(`
Array [
"GET /api/auth/csrf",
Expand All @@ -117,7 +112,7 @@ test("will refetch the session if told to do so programmatically from another wi
})
})

test("allows to customize how often the session will be re-fetched through polling", async () => {
test("allows to customize how often the session will be re-fetched through polling", () => {
jest.useFakeTimers()

fetchSpy.mockClear()
Expand All @@ -129,19 +124,57 @@ test("allows to customize how often the session will be re-fetched through polli

jest.advanceTimersByTime(1000)

await waitFor(() => {
expect(fetchSpy).toHaveBeenCalledTimes(1)
expect(fetchSpy).toHaveBeenCalledWith(
"/api/auth/session",
expect.anything()
)
})
expect(fetchSpy).toHaveBeenCalledTimes(1)
expect(fetchSpy).toHaveBeenCalledWith("/api/auth/session", expect.anything())

jest.advanceTimersByTime(1000)

jest.useRealTimers()
// it should have tried to refetch the session, hence counting 2 calls to the session endpoint
expect(fetchSpy).toHaveBeenCalledTimes(2)
expect(printFetchCalls(fetchSpy.mock.calls)).toMatchInlineSnapshot(`
Array [
"GET /api/auth/session",
"GET /api/auth/session",
]
`)
})

// TODO: Un-skip this once we know how to clear the session cache...
test.skip("allows to customize the URL for session fetching", async () => {
test.skip("allows to customize session stale time", () => {
jest.useFakeTimers()

fetchSpy.mockClear()

// session will become stale once 3s pass
render(<ProviderFlow session={mockSession} stateTime={3} />)

// we provided a mock session so it shouldn't try to fetch a new one
expect(fetchSpy).not.toHaveBeenCalled()

// make 1s pass
jest.advanceTimersByTime(1000)

// trigger a session check
changeTabVisibility("hidden")
changeTabVisibility("visible")

// should use the cached one and don't try to refetch a new one
expect(fetchSpy).not.toHaveBeenCalled()

// 3s have elapsed, the session should be stale now...
jest.advanceTimersByTime(2000)

// trigger a session check
changeTabVisibility("hidden")
changeTabVisibility("visible")

// the session was stale and should have been re-fetched
expect(fetchSpy).toHaveBeenCalledTimes(1)
expect(fetchSpy).toHaveBeenCalledWith("/api/auth/session", expect.anything())
})

test.todo(`what happens if you set "refetchInterval=1" and "staleTime=3"?`)

test("allows to customize the URL for session fetching", async () => {
fetchSpy.mockClear()

const myPath = "/api/v1/auth"
Expand All @@ -154,6 +187,13 @@ test.skip("allows to customize the URL for session fetching", async () => {

render(<ProviderFlow session={mockSession} basePath={myPath} />)

// there's an existing session so it should not try to fetch a new one
expect(fetchSpy).not.toHaveBeenCalled()

// force a session refetch across all clients...
// TODO: remove this if we ever have a way to clean the cache in-memory session state...
getSession()

return waitFor(() => {
expect(fetchSpy).toHaveBeenCalledTimes(1)
expect(fetchSpy).toHaveBeenCalledWith(
Expand All @@ -163,8 +203,6 @@ test.skip("allows to customize the URL for session fetching", async () => {
})
})

test.todo("allows to customize session stale time")

function ProviderFlow(props) {
return (
<SessionProvider {...props}>
Expand All @@ -178,7 +216,7 @@ function SessionConsumer({ testId = 1 }) {
const { data: session, status } = useSession()

return (
<div data-testid={`session-consumer-${testId}`}>
<div data-testid={`session-${testId}`}>
{status === "loading" ? "loading" : JSON.stringify(session)}
</div>
)
Expand Down
28 changes: 9 additions & 19 deletions src/client/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,8 @@ export async function signOut(options = {}) {

/** @param {import("types/react-client").SessionProviderProps} props */
export function SessionProvider(props) {
const { children, baseUrl, basePath, staleTime = 0 } = props
const { children, basePath, staleTime = 0 } = props

if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
if (basePath) __NEXTAUTH.basePath = basePath

/**
Expand All @@ -214,33 +213,24 @@ export function SessionProvider(props) {
__NEXTAUTH._getSession = async ({ event } = {}) => {
try {
const storageEvent = event === "storage"
// We should always update if we don't have a client session yet
// or if there are events from other tabs/windows
if (storageEvent || __NEXTAUTH._session === undefined) {
const forceUpdate = storageEvent || __NEXTAUTH._session === undefined
const neverStale = staleTime === 0 && !event
const unAuthenticated = staleTime > 0 && __NEXTAUTH._session === null
const notStale =
staleTime > 0 && _now() < __NEXTAUTH._lastSync + staleTime

if (forceUpdate) {
__NEXTAUTH._lastSync = _now()
__NEXTAUTH._session = await getSession({
broadcast: !storageEvent,
})
setSession(__NEXTAUTH._session)
return
}
if (
// If there is no time defined for when a session should be considered
// stale, then it's okay to use the value we have until an event is
// triggered which updates it
(staleTime === 0 && !event) ||
// If the client doesn't have a session then we don't need to call
// the server to check if it does (if they have signed in via another
// tab or window that will come through as a "storage" event
// event anyway)
(staleTime > 0 && __NEXTAUTH._session === null) ||
// Bail out early if the client session is not stale yet
(staleTime > 0 && _now() < __NEXTAUTH._lastSync + staleTime)
) {
if (neverStale || unAuthenticated || notStale) {
return
}

// An event or session staleness occurred, update the client session.
__NEXTAUTH._lastSync = _now()
__NEXTAUTH._session = await getSession()
setSession(__NEXTAUTH._session)
Expand Down

0 comments on commit 34ffda9

Please sign in to comment.