hocino
hocino•2w ago

Understand how Context works in test environment with Identity

Hi Everyone, I'm a bit confused about the context in my test. Here's what I'm trying to achieve: I want to check if a user can access a document created by another user. To do this: 1 - I created an identity (asViewer) for the user (viewer) who wants to access the document. 2 - Then, I verified that the owner of the document can retrieve this user by using t.run. This part works fine. 3 - Next, I called my mutation, which is responsible for adding permissions to the document for the viewer. I executed this mutation with the asOwner identity, but it throws an error. The mutation is unable to retrieve the viewer user. Here is the relevant part of my code: const { asUser: asViewer, user: viewer } = await createIdentity(t); const getViewerAsOwner = await asOwner.run(async (ctx) => { return await ctx.db.get(viewer._id); }); asOwner.mutation(api.core.permissions.insert, { subjectId: viewer._id, entityId: ownedApplication._id, entityType: 'applications', role: 'viewer', }); I don't understand why I'm able to retrieve the viewer user when I execute asOwner.run, but not when I execute asOwner.mutation. Did I miss something? Could someone explain this behavior to me? Thank you
16 Replies
Convex Bot
Convex Bot•2w ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
sshader
sshader•2w ago
The code using t.run isn't performing any access checks (it's just doing a db.get). If you're using customFunctions / customCtx for your access checks, that ctx is not the one that's used in t.run t.run is often useful for setting things up in a test without needing to get all the access control right + write extra functions just for the sake of test set up. What does the code in your mutation look like? What happens when you call that code inside your t.run?
hocino
hocinoOP•2w ago
Hi @sshader , Thanks for your reply Just to clarify, I use t.run just to confirm asOwner can get the user (my test doesn't need to perform this t.run) My mutation needs to get the viewer user, and this is what it throws the error. It's exactly the same code than the t.run execute but inside my mutation the code throw an error because it doesn't find the viewer._id This is why I am confused. I thought maybe the t.run was with a different context const getViewerAsOwner = await asOwner.run(async (ctx) => { return await ctx.db.get(viewer._id); }); console.log('getViewerAsOwner', getViewerAsOwner ) => getViewerAsOwner { _creationTime: 1733937360453, _id: '10003;users', email: 'dFVWE@test.com', name: 'dFVWE' } In my mutation const subject = await ctx.db.get(args.subjectId); console.log(subject) => null (subjectId has the same value than viewerId) So I execute 2 times the same query but I don't have the same result.
sshader
sshader•2w ago
Ooh ok gotcha -- just to narrow down the problem, if you create a mutation that just does db.get(args.subjectId), do you still observe the same issue? (t.run returns an object but t.mutation does not?) Also are you using customFunctions / customCtx at all?
hocino
hocinoOP•2w ago
Yes, my mutation is a customFunction which returns a customCtx So, I created a mutation just with the db.get as follow : export const inserttest = mutation({ args: { subjectId: v.union(v.id(USERS_TABLE), v.id(TEAMS_TABLE)) }, handler: async (ctx, args) => { const subject = await ctx.db.get(args.subjectId); if (!subject) throw 'Subject not found TEST'; } }); And I have the same error. it's a 'classic' convex mutation not a custom function
sshader
sshader•2w ago
Cool thanks for the extra testing -- what does createIdentity do? (I set up something similar on my end and both things are returning as expected
const sessionA = await t.mutation(api.sessions.create, {})
console.log(
'from mutation',
await t.mutation(internal.sessions.test, { sessionId: sessionA })
)
console.log(
'from run',
await t.run((ctx) => {
return ctx.db.get(sessionA)
})
const sessionA = await t.mutation(api.sessions.create, {})
console.log(
'from mutation',
await t.mutation(internal.sessions.test, { sessionId: sessionA })
)
console.log(
'from run',
await t.run((ctx) => {
return ctx.db.get(sessionA)
})
)
hocino
hocinoOP•2w ago
it's just an helper to create an identity export async function createUser(t: TestConvex<any>) { const name = generateRandomString(5); return await t.run(async (ctx) => { const id = await ctx.db.insert(USERS_TABLE, { email: ${name}@test.com, name }); return (await ctx.db.get(id)) as Doc<'users'>; }); } export async function createIdentity(t: TestConvex<any>) { const user = await createUser(t); const asUser = t.withIdentity({ subject: user!._id, tokenIdentifier: user!._id, name: user.name }); return { user, asUser }; } Can the withIdentity could be the cause ?
sshader
sshader•2w ago
Hmm I still can't reproduce this with your createIdentity function and with asUser I'm on the latest version of convex-test (0.0.34)
hocino
hocinoOP•2w ago
Ok, I use the same convex-test vesion What do you think about my custom Mutation export const SecureMutationBuilder = customMutation(mutation, { args: {}, input: async (ctx, args) => { const { userId, userPermissions } = await getPermissions(ctx); const db = wrapDatabaseWriter( ctx, ctx.db, await rlsRules(ctx, userId, userPermissions) ); return { ctx: { db, userId, userPermissions }, args }; } }); Do you notice something wrong or a bad approach ? Thank you for your help
sshader
sshader•2w ago
Nothing stands out to me, but I thought you had an example of a non-custom mutation not returning the document earlier? run will use the basic convex ctx while mutation will use your custom mutation code, which could explain differences in behavior. But I'd expect run and a mutation using the basic _generated/server mutation wrapper to behave the same.
hocino
hocinoOP•2w ago
Indeed 😅 the non-custom mutation not returns the document. I confirm I don't have the same database in function of where I question the database. Here all my code : test( testName( '#030 SERVER Retrieve an object if the current user has a role of viewer, contributor, admin, or owner on the object.' ), async () => { const { t, asUser: asOwner } = await initCtxAndUser(); const ownedApp = { name: 'My app', description: 'My app description' }; const ownedAppId = await asOwner.mutation( api.services.applications.insert, ownedApp ); const ownedApplication = await asOwner.query( api.services.applications.findOne, { id: ownedAppId } ); // Test for owner role expect(ownedApplication).toBeDefined(); expect(ownedApplication).not.toBeNull(); const { asUser: asViewer, user: viewer } = await createIdentity(t); await expect( asViewer.query(api.services.applications.findOne, { id: ownedApplication._id }) ).rejects.toThrow('entity.getEntityById - Method not allowed.'); const users = await t.run(async (ctx) => { return await ctx.db.query('users').collect(); }); console.log('users', users); /*=> users [ { _creationTime: 1733946484096, _id: '10000;users', email: 'PumeX@test.com', name: 'PumeX' }, { _creationTime: 1733946485074, _id: '10003;users', email: 'czMzP@test.com', name: 'czMzP' } ]*/ asOwner.mutation(api.core.permissions.inserttest, { subjectId: viewer._id }); const viewedApplication = await asViewer.query( api.services.applications.findOne, { id: ownedApplication._id } ); // Test for viewer role expect(viewedApplication).toBeDefined(); expect(viewedApplication).not.toBeNull(); } ); Here my mutation (a non-custom mutation) export const inserttest = mutation({ args: { subjectId: v.union(v.id(USERS_TABLE), v.id(TEAMS_TABLE)) }, handler: async (ctx, args) => { const f = await ctx.db.query('users').collect(); console.log('users', f) /* => users [ { _creationTime: 1733946485287, _id: '10000;users', email: 'Div9H@test.com', name: 'Div9H' } ] */ const subject = await ctx.db.get(args.subjectId); if (!subject) throw 'Subject not found TEST'; } }); When I call inserttest asOwner, the mutation doesn't know the viewer user. I read many times my code I didn't find anything bad but if it works fine in your case something must be wrong in my way to implement this test
sshader
sshader•2w ago
Thanks for sharing your code -- this all seems pretty strange, and thanks for helping debug. It almost looks like we're getting multiple instances of t / convex-test here -- if those logs are all from a single run, we have two different documents with _id 10000;users. Anything interesting in initCtxAndUser? Also if you're comfortable sharing more of your code with me, feel free to DM me and I'd be happy to take a look
hocino
hocinoOP•2w ago
Yes it's from a single run here the initCtxAndUser code async function initCtxAndUser() { const t = convexTest(schema); const componentModules = import.meta.glob( '../../../node_modules/@convex-dev/sharded-counter/src/component/**/*.ts' ); t.registerComponent('shardedCounter', shardedSchema, componentModules); const { asUser, user } = await createIdentity(t); return { t, asUser, user }; } Oh I found the problem Just a missing 'await' 😱 Sorry for the time Thank you
sshader
sshader•2w ago
Lol glad you're unblocked!
sshader
sshader•2w ago
https://typescript-eslint.io/rules/no-floating-promises/ is pretty good (I forget to await my promises all the time without it)
no-floating-promises | typescript-eslint
Require Promise-like statements to be handled appropriately.
hocino
hocinoOP•2d ago
Excellent. Thank you @sshader 😊

Did you find this page helpful?