@@ -53,18 +53,40 @@ import { loader as projectsLoader } from "~/routes/api.v1.projects";
5353
5454const USER_ID = "usr_1" ;
5555
56- async function createOrg ( cap : string [ ] ) : Promise < { status : number ; body : any } > {
56+ // Counts `can()` invocations without changing what the ability answers.
57+ function countingAbility ( ability : any ) : { ability : any ; canCalls : ( ) => number } {
58+ let canCalls = 0 ;
59+ const wrapped = new Proxy ( ability , {
60+ get ( target , prop , receiver ) {
61+ const value = Reflect . get ( target , prop , receiver ) ;
62+ if ( typeof value !== "function" ) {
63+ return value ;
64+ }
65+ if ( prop === "can" ) {
66+ return ( ...args : any [ ] ) => {
67+ canCalls ++ ;
68+ return value . apply ( target , args ) ;
69+ } ;
70+ }
71+ return value . bind ( target ) ;
72+ } ,
73+ } ) ;
74+ return { ability : wrapped , canCalls : ( ) => canCalls } ;
75+ }
76+
77+ async function createOrg ( cap : string [ ] ) : Promise < { status : number ; body : any ; canCalls : number } > {
5778 const token = await signUserActorToken ( SESSION_SECRET , {
5879 userId : USER_ID ,
5980 client : "personal-access-token" ,
6081 cap,
6182 } ) ;
83+ const counted = countingAbility ( buildJwtAbility ( cap ) ) ;
6284 mocks . authenticateUserActor . mockImplementation ( async ( ) => ( {
6385 ok : true ,
6486 userId : USER_ID ,
6587 claims : { userId : USER_ID , client : "personal-access-token" , cap } ,
6688 subject : { type : "userActor" , userId : USER_ID , organizationId : "org_1" } ,
67- ability : buildJwtAbility ( cap ) ,
89+ ability : counted . ability ,
6890 } ) ) ;
6991
7092 const response = await action ( {
@@ -76,19 +98,26 @@ async function createOrg(cap: string[]): Promise<{ status: number; body: any }>
7698 params : { } ,
7799 context : { } ,
78100 } as any ) ;
79- return { status : response . status , body : await response . json ( ) } ;
101+ return { status : response . status , body : await response . json ( ) , canCalls : counted . canCalls ( ) } ;
80102}
81103
82104// An ordinary PAT, paired with an ability that denies everything. Nothing on this route may
83105// consult it — the route has no org to scope a gate to, and on cloud the plugin returns a
84106// deny-shaped ability when there is no org context.
85- async function createOrgWithPat ( ) : Promise < { status : number ; body : any } > {
107+ async function createOrgWithPat ( ) : Promise < { status : number ; body : any ; canCalls : number } > {
108+ let canCalls = 0 ;
86109 mocks . authenticatePat . mockImplementation ( async ( ) => ( {
87110 ok : true ,
88111 userId : USER_ID ,
89112 tokenId : "pat_1" ,
90113 lastAccessedAt : new Date ( ) ,
91- ability : { can : ( ) => false , canSuper : ( ) => false } ,
114+ ability : {
115+ can : ( ) => {
116+ canCalls ++ ;
117+ return false ;
118+ } ,
119+ canSuper : ( ) => false ,
120+ } ,
92121 } ) ) ;
93122
94123 const response = await action ( {
@@ -100,7 +129,7 @@ async function createOrgWithPat(): Promise<{ status: number; body: any }> {
100129 params : { } ,
101130 context : { } ,
102131 } as any ) ;
103- return { status : response . status , body : await response . json ( ) } ;
132+ return { status : response . status , body : await response . json ( ) , canCalls } ;
104133}
105134
106135const AGENT_ENVIRONMENT_ID = "env_dev" ;
@@ -177,6 +206,7 @@ describe("creating an organization over the API", () => {
177206
178207 expect ( result . status ) . toBe ( 201 ) ;
179208 expect ( result . body . slug ) . toBe ( "new-org" ) ;
209+ expect ( result . canCalls ) . toBe ( 0 ) ;
180210 } ) ;
181211
182212 // The env gate runs before the capability gate, so an install with the API disabled tells
@@ -189,6 +219,7 @@ describe("creating an organization over the API", () => {
189219 const result = await createOrg ( [ "read:all" ] ) ;
190220
191221 expect ( result . status ) . toBe ( 404 ) ;
222+ expect ( result . canCalls ) . toBe ( 0 ) ;
192223 expect ( mocks . createOrganization ) . not . toHaveBeenCalled ( ) ;
193224 } finally {
194225 mocks . env . ORG_CREATION_API_ENABLED = "1" ;
0 commit comments