Closed flybayer closed 1 year ago
This is something I'm toying with as a solution, but I'm not 100% on it yet. Example usage:
# changePassword.test.ts
const currentPassword = "Password01"
const newPassword = "Password02"
const user = await db.user.create({
data: {
email: "user@example.com",
hashedPassword: await SecurePassword.hash(currentPassword),
},
})
+ const { res } = await createMockContext({ userId: user.id })
const result = changePassword({ currentPassword, newPassword }, res.blitzCtx)
await expect(result).resolves.toBe(true)
Implementation is below. Still have some testing to do on it.
jest.fn()
calls on all the properties, since that doesn't create a great dev experience when you try to interact with it. The alternative is to bring in a new dependency to construct mocks for req
and res
because they're not easy to construct otherwise.node-mocks-http
for mocking req and res, but I need to look and see if there is a better alternative.import {
sessionMiddleware,
getSession,
MiddlewareRequest as Req,
MiddlewareResponse as Res,
Ctx,
} from "blitz"
import httpMocks from "node-mocks-http"
interface CreateMockContextOptions {
userId?: number
reqOptions?: httpMocks.RequestOptions
resOptions?: httpMocks.ResponseOptions
}
// Creates a mock context for use in tests and scripts. Attempts to make it the
// "real deal" by calling the same initialization logic that creates actual
// session contexts.
// If you're here because this function isn't working anymore, then the
// commented steps below may require special attention.
export default async function createMockContext<C extends Ctx>(
options: CreateMockContextOptions = {}
) {
const { userId, reqOptions, resOptions } = options
const { req: mockReq, res: mockRes } = httpMocks.createMocks<Req, Res<C>>(reqOptions, resOptions)
// Constructs global.sessionConfig which is referenced in several utilities.
// I know, this doesn't actually run the middleware. The initialization logic
// has the side effect we need.
sessionMiddleware({ isAuthorized: () => userId != null })
// Ensures the response has the blitzCtx object which is required for
// authorization checks.
await getSession(mockReq, mockRes)
// Simulate the session is authorized to a user, if an ID was provided.
if (userId) mockRes.blitzCtx.session.$publicData.userId = userId
return { req: mockReq, res: mockRes }
}
This was extremely helpful! I made a couple of changes to your script in order to make sure that the simulation is as close to the real thing as possible. Also, httpMocks.createMocks<Req, Res<C>>
did not typecheck for me, so I had to use any
.
The most important change I did was to import blitz.config.js
instead of calling sessionMiddleware
. This ensures that the correct function for isAuthorized
is used.
Secondly, I created a function getPublicSessionData
which is used by the mock script and by the actual login mutation in app/auth/mutations/login.ts
(or wherever you do your login).
The result is the following:
// app/auth/utils/getPublicSessionData.ts
import { Role } from "../../../types"
import { User } from "db"
export function getPublicSessionData(user: Pick<User, "id" | "role">) {
return { userId: user.id, role: user.role as Role }
}
// test/createMockContext.ts
import { Ctx, getSession, MiddlewareRequest as Req, MiddlewareResponse as Res } from "blitz"
import httpMocks from "node-mocks-http"
import { User } from "db"
// This import is crucial, as it modifies global state by calling sessionMiddleware
// Most importantly, this sets the isAuthorized method in global.sessionConfig
import "../blitz.config"
import { getPublicSessionData } from "../app/auth/utils/getPublicSessionData"
interface CreateMockContextOptions {
user?: User
reqOptions?: httpMocks.RequestOptions
resOptions?: httpMocks.ResponseOptions
}
// Based on https://github.com/blitz-js/blitz/issues/2654#issuecomment-904426530
// Creates a mock context for use in tests and scripts. Attempts to make it the
// "real deal" by calling the same initialization logic that creates actual
// session contexts.
export default async function createMockContext<C extends Ctx>({
user,
reqOptions,
resOptions,
}: CreateMockContextOptions = {}) {
const mocks = httpMocks.createMocks<any, any>(reqOptions, resOptions)
const mockReq: Req = mocks.req
const mockRes: Res<C> = mocks.res
// Ensures the response has the blitzCtx object which is required for
// authorization checks
await getSession(mockReq, mockRes)
// Simulate login by saving public session data
if (user) {
// Need to use Object.assign instead of spread operator,
// because $publicData is readonly (only has a getter)
Object.assign(mockRes.blitzCtx.session.$publicData, getPublicSessionData(user))
}
return { req: mockReq, res: mockRes, ctx: mockRes.blitzCtx }
}
// app/auth/mutations/login.ts
import { Login } from "../validations"
import { getPublicSessionData } from "../utils/getPublicSessionData"
// .. more code ...
export default resolver.pipe(resolver.zod(Login), async ({ email, password }, ctx) => {
// .. more code ...
await ctx.session.$create(getPublicSessionData(user))
return user
})
Then you can use it like this:
import db from "db"
import getUsers from "./getUsers"
import createMockContext from "../../../test/createMockContext"
beforeEach(async () => {
await db.$reset()
})
describe("getUsers query", () => {
it("rejects access for regular users", async () => {
const user = await db.user.create({
data: {
email: "user@example.com",
role: "USER",
},
})
const { ctx } = await createMockContext({ user })
await expect(async () => {
await getUsers({}, ctx)
}).rejects.toThrow("You are not authorized to access this")
})
})
@vincentrolfs Looks nice. What typescript error did you see? I didn't get any type errors when I tried it.
If this is going to be a core utility, I think it has to avoid importing User from db
since there's no guarantee that client applications will have that model and I don't think a core utility would be able to import the app's db client anyway. I suppose we could make it a recipe instead?
@vincentrolfs thanks a lot for the snippet! really helpful, would be great to have this documented on the blitz website, for those who want to test the queries/mutations, independently from UI
Glad to help: https://github.com/blitz-js/blitzjs.com/pull/642
Thanks a lot for these examples! With a quick look I didn't find an example of a happy path. Nothing special about it so more like documentation for people who end up reading this conversation in the future 😄
it("returns customers if user has authenticated", async () => {
const customer = await db.customer.create({ data: { name: "test" } })
const user = await db.user.create({
data: {
email: "user@example.com",
role: "USER",
},
})
const { ctx } = await createMockContext({ user })
await ctx.session.$create({ userId: user.id, role: user.role as Role })
await expect(getCustomers({}, ctx)).resolves.toEqual({
count: 1,
customers: [customer],
hasMore: false,
nextPage: null,
})
})
For some reason this throws en error Internal Blitz Error: process.env.BLITZ_APP_DIR is not set
. To fix this I had to add BLITZ_APP_DIR=.
in .env.test.local
. I guess adding this env variable shouldn't be needed?
❯ blitz -v
macOS Monterey | darwin-x64 | Node: v16.14.0
blitz: 0.45.4 (global)
blitz: 0.45.4 (local)
Package manager: npm
System:
OS: macOS 12.4
CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
Memory: 3.23 GB / 32.00 GB
Shell: 5.8.1 - /bin/zsh
Binaries:
Node: 16.14.0 - ~/.asdf/installs/nodejs/16.14.0/bin/node
Yarn: 1.22.18 - ~/.asdf/installs/nodejs/16.14.0/.npm/bin/yarn
npm: 8.3.1 - ~/.asdf/installs/nodejs/16.14.0/bin/npm
Watchman: Not Found
npmPackages:
@prisma/client: 3.16.0-integration-tmp-revert-node14.1 => 3.16.0-integration-tmp-revert-node14.1
blitz: 0.45.4 => 0.45.4
prisma: 3.16.0-integration-tmp-revert-node14.1 => 3.16.0-integration-tmp-revert-node14.1
react: 18.0.0 => 18.0.0
react-dom: 18.0.0 => 18.0.0
typescript: ~4.5 => 4.5.5
Anyone knows if/how this could work with the new Blitz 2.0 next toolkit?
Got as far as this with a couple of edits (mainly to imports and types) to the latest example above:
Internal Blitz Error: globalThis.__BLITZ_SESSION_COOKIE_PREFIX is not set
31 | // Ensures the response has the blitzCtx object which is required for
32 | // authorization checks
> 33 | await getSession(mockReq, mockRes)
| ^
34 |
35 | // Simulate login by saving public session data
36 | if (user) {
// createMockContext.ts
import { Ctx, MiddlewareResponse as Res } from 'blitz'
import { getSession } from '@blitzjs/auth'
import { NextApiRequest as Req } from 'next'
...
import '../next.config'
EDIT:
Seems like adding
import { api } from 'app/blitz-server'
...
await api(() => null)
await getSession(mockReq, mockRes)
Fixes these issues.. though not sure if that's a correct way at all
Thank you - this works great using createMockContext()
for my query and mutation tests.
I'm now trying to mock Blitz context on some React component tests that call mutations/queries via useQuery
. One of these mutations uses ctx.session.$setPublicData
. Unfortunately ctx.session
equals { fromInvoke: true }
and is missing the $setPublicData
method. I'm using Blitz 2.0.6 and pages router. Any ideas how I can populate this?
What do you want and why?
Currently you have to manually craft a
ctx
mock for using in tests or running queries/mutations insideblitz console
.We need to make this better.
Current workaround: https://github.com/blitz-js/blitz/discussions/2653
Possible implementation(s)
Maybe add a new
createMockCtx()
utility to Blitz?