@@ -29,13 +29,15 @@ const {
2929 mockGetRateLimit,
3030 mockGetUserEntityPermissions,
3131 mockGetWorkspaceBillingSettings,
32+ mockGetWorkspaceBilledAccountUserId,
3233} = vi . hoisted ( ( ) => ( {
3334 mockAuthenticateV1Request : vi . fn ( ) ,
3435 mockGetSubscription : vi . fn ( ) ,
3536 mockCheckRateLimit : vi . fn ( ) ,
3637 mockGetRateLimit : vi . fn ( ) ,
3738 mockGetUserEntityPermissions : vi . fn ( ) ,
3839 mockGetWorkspaceBillingSettings : vi . fn ( ) ,
40+ mockGetWorkspaceBilledAccountUserId : vi . fn ( ) ,
3941} ) )
4042
4143vi . mock ( '@/app/api/v1/auth' , ( ) => ( {
@@ -61,7 +63,7 @@ vi.mock('@/lib/workspaces/permissions/utils', () => ({
6163
6264vi . mock ( '@/lib/workspaces/utils' , ( ) => ( {
6365 getWorkspaceBillingSettings : mockGetWorkspaceBillingSettings ,
64- getWorkspaceBilledAccountUserId : vi . fn ( async ( ) => 'billed-user' ) ,
66+ getWorkspaceBilledAccountUserId : mockGetWorkspaceBilledAccountUserId ,
6567} ) )
6668
6769import { DEFAULT_PERMISSION_GROUP_CONFIG } from '@/lib/permission-groups/fields'
@@ -70,6 +72,7 @@ import {
7072 checkRateLimit ,
7173 checkWorkspaceScope ,
7274 createRateLimitResponse ,
75+ requireWorkspaceRequestActor ,
7376 v1ValidationErrorResponse ,
7477} from '@/app/api/v1/middleware'
7578
@@ -421,3 +424,47 @@ describe('checkWorkspaceScope', () => {
421424 expect ( response ?. status ) . toBe ( 403 )
422425 } )
423426} )
427+
428+ describe ( 'requireWorkspaceRequestActor' , ( ) => {
429+ beforeEach ( ( ) => {
430+ vi . clearAllMocks ( )
431+ mockGetWorkspaceBilledAccountUserId . mockResolvedValue ( 'billed-user' )
432+ } )
433+
434+ it ( 'substitutes the billed account as the system actor for a workspace key' , async ( ) => {
435+ const actor = await requireWorkspaceRequestActor (
436+ { allowed : true , keyType : 'workspace' , userId : 'key-creator' } as never ,
437+ 'workspace-1'
438+ )
439+
440+ expect ( actor ) . toEqual ( { ok : true , actorUserId : 'billed-user' } )
441+ } )
442+
443+ it ( 'keeps the owner for a personal key' , async ( ) => {
444+ const actor = await requireWorkspaceRequestActor (
445+ { allowed : true , keyType : 'personal' , userId : 'user-1' } as never ,
446+ 'workspace-1'
447+ )
448+
449+ expect ( actor ) . toEqual ( { ok : true , actorUserId : 'user-1' } )
450+ } )
451+
452+ /**
453+ * An archived or deleted workspace has no billed account to stand in. That is
454+ * a reachable request about an unreachable workspace, not a server fault: the
455+ * call sites used to throw, and the routes' catch-all reported it as a 500.
456+ */
457+ it ( 'projects an unresolvable actor onto a 400 rather than throwing' , async ( ) => {
458+ mockGetWorkspaceBilledAccountUserId . mockResolvedValue ( null )
459+
460+ const actor = await requireWorkspaceRequestActor (
461+ { allowed : true , keyType : 'workspace' , userId : 'key-creator' } as never ,
462+ 'workspace-gone'
463+ )
464+
465+ expect ( actor . ok ) . toBe ( false )
466+ if ( actor . ok ) throw new Error ( 'expected a refusal' )
467+ expect ( actor . response . status ) . toBe ( 400 )
468+ await expect ( actor . response . json ( ) ) . resolves . toEqual ( { error : 'Invalid workspace ID' } )
469+ } )
470+ } )
0 commit comments