#convex test user identity

23 messages · Page 1 of 1 (latest)

torpid hearth
#

i use a custom context with a custom query. when i now use convex test, how can i set that custom context, so my regular functions have access to that context that i want to test?

storm burrowBOT
#

Hi @torpid hearth. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

convex-test only lets you control two things about the context directly:

  1. The auth identity via t.withIdentity(...). [convex-test auth]
  2. The raw Convex ctx inside t.run(async ctx => { ... }). [convex-test using]

Your custom context (e.g. from customQuery/customCtx in convex-helpers) is built by your own wrapper, so you don’t “set” it in convex-test; you exercise it by calling the wrapped functions exactly as your app would.

If your custom context depends only on auth

If your customCtx just uses ctx.auth.getUserIdentity() (or similar), then in tests you do:

const t = convexTest(schema, modules);

// Simulate a logged-in user
const asUser = t.withIdentity({ subject: "user1", issuer: "test" });

// Call your custom query/mutation that builds the custom ctx internally
const result = await asUser.query(api.myModule.myCustomQuery, { /* args */ });

Your customQuery wrapper will run, build the custom context (e.g. ctx.user, wrapped ctx.db, etc.), and your handler will see that context. [custom functions; auth in functions; convex-test auth]

If you need to test the helper that builds custom ctx

If you have a separate helper (like in the RLS examples) that takes a base ctx and returns extra fields, you can call it from inside t.run:

const t = convexTest(schema, modules);
#

await t.run(async (ctx) => {
// ctx here is a normal server ctx (with db, auth, etc.)
const custom = await myCustomCtxBuilder(ctx); // your customCtx logic
// Now test behavior that uses custom.db, custom.user, etc.
});


`convex-test` doesn’t expose a way to pre‑inject arbitrary extra fields into the ctx of a function call; instead, your custom wrapper is responsible for constructing that context, and you test it by:

- Using `withIdentity` to control auth when needed.
- Calling the wrapped functions (or the ctx‑builder) so they construct the custom context as in production. [[row-level security ctx](<https://stack.convex.dev/row-level-security#extending-access-functions>); [custom functions](<https://stack.convex.dev/custom-functions#how-do-we-do-it>); [convex-test using](<https://docs.convex.dev/testing/convex-test#using-convex-test>)]

If you share a small snippet of how you define your `customQuery` / `customCtx`, I can map this pattern to your exact code.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
#

@torpid hearth Kapa.ai is still learning and improving, please let me know how I did by reacting below.

torpid hearth
#

@storm burrow export const authedUserQuery = zCustomQuery(
_query,
customCtx(async (ctx) => {
const user = await User.authenticate({ ctx });
return {
...ctx,
auth: {
...ctx.auth,
user,
},
};
}),
);

how can i have the ctx.auth.user in the tests

storm burrowBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You don’t set ctx.auth.user directly in convex-test; you let your authedUserQuery wrapper build it, and you control only the identity it sees.

convex-test lets you create a test handle “as a user” via withIdentity, which affects what ctx.auth.getUserIdentity() returns inside your Convex functions. [convex-test auth] Your User.authenticate({ ctx }) should use that identity to look up the user and then your customCtx adds auth.user:

export const authedUserQuery = zCustomQuery(
  _query,
  customCtx(async (ctx) => {
    const user = await User.authenticate({ ctx });
    return {
      ...ctx,
      auth: {
        ...ctx.auth,
        user,
      },
    };
  }),
);

In tests:

import { convexTest } from "convex-test";
import { api } from "./_generated/api";
import schema from "./schema";
import { modules } from "./test.setup"; // if you use modules

test("ctx.auth.user is set", async () => {
  const t = convexTest(schema, modules);

  // Choose identity fields that your User.authenticate understands
  const asUser = t.withIdentity({
    subject: "user-1",
    issuer: "test-issuer",
    name: "Test User",
  });

  // Call a function defined with authedUserQuery
  const result = await asUser.query(api.someModule.someAuthedQuery, {
    /* args */
  });

  // Inside someAuthedQuery's handler, ctx.auth.user will be populated
  // You assert via its observable behavior (result, db writes, etc.)
});

So:

#
  • withIdentity(...) → controls ctx.auth.getUserIdentity() in tests. [convex-test auth]
  • Your existing User.authenticate({ ctx }) runs as usual and sets auth.user via customCtx.
  • Any query/mutation built with authedUserQuery will see ctx.auth.user in tests exactly like in production.

If you need to unit‑test User.authenticate itself, you can also call it inside t.run(async ctx => { ... }), after creating an identity with withIdentity. [convex-test using]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@torpid hearth Kapa.ai is still learning and improving, please let me know how I did by reacting below.

torpid hearth
#

@storm burrow const asAuthedUser = t.withIdentity({
auth: {
user: identity,
},
});

Type '{ user: UserIdentity; }' is not assignable to type 'JSONValue | undefined'.
Types of property 'user' are incompatible.
Type 'UserIdentity' is not assignable to type 'JSONValue | undefined'.
Type 'UserIdentity' is missing the following properties from type 'JSONValue[]': length, pop, push, concat, and 35 more.

storm burrowBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

withIdentity only accepts a plain UserIdentity-shaped object (i.e. what ctx.auth.getUserIdentity() returns), not a nested { auth: { user: ... } } object. Its parameter type is UserIdentity & { [key: string]: JSONValue }, so everything must be JSON-serializable and at the top level. [convex-test auth; UserIdentity api]

So instead of:

const asAuthedUser = t.withIdentity({
  auth: {
    user: identity,
  },
});

you should pass the identity fields directly:

const asAuthedUser = t.withIdentity({
  // whatever your User.authenticate reads:
  subject: "user-1",
  issuer: "test-issuer",
  tokenIdentifier: "test-issuer|user-1", // optional, auto-generated if omitted
  // plus any extra claims you want:
  email: "user@example.com",
  name: "Test User",
});

Then, inside your Convex code:

  • ctx.auth.getUserIdentity() will return that object. [convex-test auth]
  • User.authenticate({ ctx }) should use it to load your user.
  • Your authedUserQuery’s customCtx will set ctx.auth.user, and your handlers can rely on it.

You never set ctx.auth.user from withIdentity; you only control the identity that your own User.authenticate uses to populate auth.user.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@torpid hearth Kapa.ai is still learning and improving, please let me know how I did by reacting below.

torpid hearth
#

@storm burrow public static async authenticate<
T extends QueryCtx | MutationCtx | ActionCtx,

({ ctx }: { ctx: T }) {
const identity = (await ctx.auth.getUserIdentity()) as
| (ConvexUserIdentity & WorkOSAuthAttributes)
| null;
if (!identity) {
throw new DetailedConvexError({
code: DetailedConvexErrorCode.UNAUTHENTICATED,
message: "Unauthenticated",
status: 401,
});
}
return {
tokenIdentifier: identity.tokenIdentifier,
issuer: identity.issuer,
id: identity.subject,
entitlements: identity.entitlements || [],
organizationId: identity.org_id || "",
permissions: identity.permissions || [],
role: identity.role || "",
roles: identity.roles || [],
sessionId: identity.sid || "",
} satisfies UserIdentity;
}

the problem is, that i get custom attributes from workos and in the test cases it just sets the default values, even if i set them in the useridentity object that i place with withIdentity

storm burrowBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You’re running into a type/shape mismatch between what WorkOS actually puts into the JWT and what convex-test’s withIdentity expects.

From the Convex side:

  • withIdentity takes a UserIdentity-like object: “user identity attributes” where every field is JSON-serializable and at the top level. [convex-test auth; UserIdentity api]
  • In functions, ctx.auth.getUserIdentity() returns that same shape. Any extra fields (custom claims) are just additional top-level properties on that object. [UserIdentity api]

So for tests, you must pass your WorkOS custom claims (like org_id, entitlements, permissions, etc.) as top-level fields to withIdentity, not nested anywhere.

Given your authenticate:

const identity = (await ctx.auth.getUserIdentity()) as
  | (ConvexUserIdentity & WorkOSAuthAttributes)
  | null;

// ...
return {
  tokenIdentifier: identity.tokenIdentifier,
  issuer: identity.issuer,
  id: identity.subject,
  entitlements: identity.entitlements || [],
  organizationId: identity.org_id || "",
  permissions: identity.permissions || [],
  role: identity.role || "",
  roles: identity.roles || [],
  sessionId: identity.sid || "",
} satisfies UserIdentity;

your test needs to mimic a WorkOS JWT payload by putting those fields directly on the identity:

const asAuthedUser = t.withIdentity({
  // required / standard fields
  subject: "user-1",
  issuer: "<https://api.workos.com/",> // or your actual issuer
  tokenIdentifier: "<https://api.workos.com/|user-1",>
#

// WorkOS custom claims your code reads:
org_id: "org_123",
entitlements: ["feature:a", "feature:b"],
permissions: ["read:foo", "write:bar"],
role: "admin",
roles: ["admin", "member"],
sid: "sess_abc",
});


Then, inside the test:

- `ctx.auth.getUserIdentity()` will return exactly that object. [[convex-test auth](<https://docs.convex.dev/testing/convex-test#authentication>)]
- Your `authenticate` will see `identity.org_id`, `identity.entitlements`, etc., and will *not* fall back to the default `|| []` / `|| ""` values.

If you’re still seeing defaults:

1. Log the identity inside the test function:

   ```ts
   export const someAuthedQuery = authedUserQuery({
     // ...
     handler: async (ctx, args) => {
       console.log(await ctx.auth.getUserIdentity());
       const user = await User.authenticate({ ctx });
       // ...
     },
   });
  1. Run the test and confirm the logged object actually contains org_id, entitlements, etc. If it doesn’t, the issue is how you’re calling withIdentity, not Convex.

The Convex docs explicitly say that any additional fields on UserIdentity are just custom claims, indexable by string key, and you can assert their type. [UserIdentity api] convex-test doesn’t strip or transform them; it just passes through what you give to withIdentity. [convex-test auth]

So the fix is: ensure your test identity object has those WorkOS fields at the top level, with JSON-serializable values.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@torpid hearth Kapa.ai is still learning and improving, please let me know how I did by reacting below.

torpid hearth
#

@storm burrow how can i then access the ctx.auth.user inside the test case itself?

storm burrowBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You can’t directly “reach into” a running Convex function’s ctx from convex-test. The library only exposes:

  • withIdentity(...) to control what ctx.auth.getUserIdentity() returns. [convex-test auth]
  • t.run(async ctx => { ... }) to get a raw Convex ctx for ad‑hoc code. [convex-test using]

Your ctx.auth.user is added by your authedUserQuery wrapper via customCtx, so there are two practical ways to test it:


1. Assert via behavior of a function using ctx.auth.user

Define a function with authedUserQuery that returns (or uses) ctx.auth.user, then assert on its result:

// convex/users.ts
export const getAuthedUser = authedUserQuery({
  args: {},
  handler: async (ctx, _args) => {
    return ctx.auth.user; // whatever your customCtx attached
  },
});

Test:

import { convexTest } from "convex-test";
import { api } from "./_generated/api";
import schema from "./schema";

test("ctx.auth.user is populated", async () => {
  const t = convexTest(schema, modules);

  const asUser = t.withIdentity({
    subject: "user-1",
    issuer: "test-issuer",
    org_id: "org_123",
    // any other custom claims your authenticate() reads
  });

  const user = await asUser.query(api.users.getAuthedUser, {});
  expect(user.organizationId).toBe("org_123");
});

Here you never access ctx.auth.user directly in the test; you verify what a function that depends on it returns.


2. Call your authenticate / customCtx logic inside t.run

If you want to test the context‑building helper itself, call it from t.run:

test("authenticate builds user from identity", async () => {
  const t = convexTest(schema, modules);
#

const asUser = t.withIdentity({
subject: "user-1",
issuer: "test-issuer",
org_id: "org_123",
});

await asUser.run(async (ctx) => {
const user = await User.authenticate({ ctx }); // uses ctx.auth.getUserIdentity()
expect(user.organizationId).toBe("org_123");
});
});


`convex-test` doesn’t provide a hook to inspect `ctx` of a specific query/mutation call, so the supported patterns are:

- Test functions that *use* `ctx.auth.user`.
- Or test the helpers (`authenticate`, customCtx) directly via `t.run`. [[custom functions](<https://stack.convex.dev/custom-functions#how-do-we-do-it>); [convex-test using](<https://docs.convex.dev/testing/convex-test#using-convex-test>)]
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------