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 you16 Replies
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!
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
?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.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?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 functionCool thanks for the extra testing -- what does
createIdentity
do?
(I set up something similar on my end and both things are returning as expected
)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 ?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)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 helpNothing 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.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 testThanks 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 lookYes 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 youLol glad you're unblocked!
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.
Excellent.
Thank you @sshader 😊