Blitz.jsで認証状態付きAPI Routesをテストする【暴力】

2024/03/09に公開

※ 暴力の実装です。厳密なBlitz.js内部仕様の考証は行っておりません

Sessionデータをモックする

この処理の前にユーザーのモックが必要ですが、 db.user.create() するだけなので割愛します。

spec/mocks/session.ts
import {
  EmptyPublicData,
  generateToken,
  hash256,
  PublicData,
  TOKEN_SEPARATOR,
} from "@blitzjs/auth"

export async function createSession(params: { userId: number }) {
  const handle = generateToken()

  // ctx.session.$publicData とかに詰まってるやつ
  const publicDataString = JSON.stringify({
    userId: params.userId,
    role: "USER"
  })

  // SessionTokenを生成する
  // SEE: https://github.com/blitz-js/blitz/blob/6f44c2334ee3665b450cea6c873d3500249b3e49/packages/blitz-auth/src/server/auth-sessions.ts#L445
  const sessionToken = Buffer.from(
    [handle, generateToken(32), hash256(publicDataString), "v0"].join(TOKEN_SEPARATOR)
  ).toString("base64")

  const antiCSRFToken = generateToken()

  await db.session.create({
    data: {
      userId: params.userId,
      hashedSessionToken: hash256(sessionToken),
      antiCSRFToken,
      publicData: publicDataString,
      expiresAt: null,
      handle,
    },
  })

  return { sessionToken, antiCSRFToken }
}

API Routesのテストを書く

API Routesのテストには next-test-api-route-handlerを利用します。

型エラーが出ますが、各自""よしなに""してあげてください

mockApiSessionがAPI Routesのハンドラ実行前に認証状態をモックする関数です

pages/api/__test__/session.test.ts
import db from "db"
import { testApiHandler } from "next-test-api-route-handler"
import { createFakeUser } from "@/spec/mocks/user"
import { createFakeSession, mockApiSession } from "@/spec/mocks/session"
import { mockApiSession } from "@/spec/helper/session"

beforeEach(async () => {
  await db.$reset()
})

describe("/api/session", () => {
  it("should return { user: null } if not authorized", async () => {
    await testApiHandler({
      pagesHandler: mockApiSession({ sessionToken: null }, sessionHandler),
      async test({ fetch }) {
        const res = await fetch({
          method: "GET",
        })

        const data = await res.json()
        expect(res.status).toBe(200)
        expect(data).toEqual({ user: null })
      },
    })
  })

  it("should return user data if authorized", async () => {
    const user = await createFakeUser()
    const session = await createFakeSession({ userId: user.id })

    await testApiHandler({
      pagesHandler: mockApiSession({ sessionToken: session.sessionToken }, sessionHandler),
      async test({ fetch }) {
        const res = await fetch({
          method: "GET",
        })

        const data = await res.json()
        expect(res.status).toBe(200)
        expect(data).toMatchObject({ user: { id: 1, isPremium: true } })
      },
    })
  })
})

いざ! mockApiSession関数で認証状態モック!!

req.cookieの${__BLITZ_SESSION_COOKIE_PREFIX}_sSessionTokenにSessionTokenなどを設定するだけです

spec/helper/session.ts
export function mockApiSession(
  {
    sessionToken,
  }: {
    sessionToken: string | null
  },
  handler: (req: NextApiRequest, res: BlitzNextApiResponse, ctx: any) => void | Promise<void>
) {
  return async (req: NextApiRequest, res: BlitzNextApiResponse) => {
    // SEE: https://github.com/blitz-js/blitz/blob/main/packages/blitz-auth/src/shared/constants.ts#L7
    const cookieSessionTokenName = `${__BLITZ_SESSION_COOKIE_PREFIX}_sSessionToken`
    const cooliePublicDataTokenName = `${__BLITZ_SESSION_COOKIE_PREFIX}_sPublicDataToken`

    if (!sessionToken) {
      delete req.cookies[cookieSessionTokenName]
      delete req.cookies[cooliePublicDataTokenName]
      return
    } else {
      req.cookies[cookieSessionTokenName] = sessionToken

      const { handle, id, hashedPublicData, version } = parseSessionToken(sessionToken)
      const persistedSession = await global.sessionConfig.getSession(handle)

      if (persistedSession?.publicData) {
        req.cookies[cooliePublicDataTokenName] = createPublicDataToken(persistedSession.publicData)
      }
    }

    await handler(req, res, res.blitzCtx)

    // SEE: https://github.com/blitz-js/blitz/blob/main/packages/blitz-auth/src/server/auth-sessions.ts#L452
    function parseSessionToken(token: string) {
      const [handle, id, hashedPublicData, version] = Buffer.from(token, "base64")
        .toString()
        .split(TOKEN_SEPARATOR)

      if (!handle || !id || !hashedPublicData || !version) {
        throw new Error("Failed to parse session token")
      }

      return {
        handle,
        id,
        hashedPublicData,
        version,
      }
    }

    function createPublicDataToken(publicData: string | PublicData | EmptyPublicData) {
      const payload = typeof publicData === "string" ? publicData : JSON.stringify(publicData)
      return Buffer.from(payload).toString("base64")
    }
  }
}

@blitzjs/authの中をあれこれコードを読んだりパクったりして実装をコピーしていたら、
「${__BLITZ_SESSION_COOKIE_PREFIX}_sSessionToken`設定するだけでよかったやんけ!!」と気づいてしまい、特になにもしていないコードになりました。

ウケるな🙂


これで認証状態付きのAPI Routesのテストができるようになりましたが、この記事は「テスト動いた❗みんな見てみて❗❗」の飛ばし記事なので、この後地獄を見るかもしれません。

お達者で!!!!

はなくら より

Discussion