@@ -62,6 +62,26 @@ async function createEnv(
6262 } ) ;
6363}
6464
65+ describe ( "RBAC fallback — root key resolution" , ( ) => {
66+ it ( "reports an unknown root key as not found" , async ( ) => {
67+ const prisma = {
68+ runtimeEnvironment : { findFirst : vi . fn ( ) . mockResolvedValue ( null ) } ,
69+ revokedApiKey : { findFirst : vi . fn ( ) . mockResolvedValue ( null ) } ,
70+ } as unknown as PrismaClient ;
71+ const rbac = makeController ( prisma ) ;
72+
73+ const result = await rbac . authenticateBearer ( bearerRequest ( "tr_prod_unknown" ) ) ;
74+
75+ expect ( result ) . toMatchObject ( {
76+ ok : false ,
77+ resolution : {
78+ credentialKind : "root_api_key" ,
79+ lookupPath : "not_found" ,
80+ } ,
81+ } ) ;
82+ } ) ;
83+ } ) ;
84+
6585describe ( "RBAC fallback — DEVELOPMENT branch pivot" , ( ) => {
6686 postgresTest ( "pivots to the named branch, carrying the parent's api key" , async ( { prisma } ) => {
6787 const { organization, project, orgMember } = await createTestOrgProjectWithMember ( prisma ) ;
@@ -145,6 +165,7 @@ describe("RBAC fallback — DEVELOPMENT branch pivot", () => {
145165 expect ( result . ok ) . toBe ( false ) ;
146166 if ( result . ok ) return ;
147167 expect ( result . status ) . toBe ( 401 ) ;
168+ expect ( result . error ) . toBe ( "No matching branch env" ) ;
148169 }
149170 ) ;
150171} ) ;
@@ -277,6 +298,34 @@ describe("RBAC fallback — additional keys", () => {
277298 expect ( result . subject ) . toMatchObject ( { type : "apiKey" , restricted : false } ) ;
278299 } ) ;
279300
301+ postgresTest ( "does not record use when branch resolution fails" , async ( { prisma } ) => {
302+ const { organization, project, orgMember, user } = await createTestOrgProjectWithMember ( prisma ) ;
303+ const rbac = makeController ( prisma ) ;
304+ const devRoot = await createEnv ( prisma , project . id , organization . id , {
305+ type : "DEVELOPMENT" ,
306+ orgMemberId : orgMember . id ,
307+ } ) ;
308+ const additional = generateAdditionalApiKey ( "DEVELOPMENT" ) . apiKey ;
309+ const created = await prisma . apiKey . create ( {
310+ data : {
311+ name : "Branch key" ,
312+ keyHash : createHash ( "sha256" ) . update ( additional ) . digest ( "hex" ) ,
313+ lastFour : additional . slice ( - 4 ) ,
314+ runtimeEnvironmentId : devRoot . id ,
315+ createdByUserId : user . id ,
316+ presetId : null ,
317+ scopes : [ "admin" ] ,
318+ } ,
319+ } ) ;
320+
321+ const result = await rbac . authenticateBearer ( bearerRequest ( additional , "missing-branch" ) ) ;
322+
323+ expect ( result ) . toMatchObject ( { ok : false , status : 401 , error : "No matching branch env" } ) ;
324+ await expect (
325+ prisma . apiKey . findUnique ( { where : { id : created . id } , select : { lastUsedAt : true } } )
326+ ) . resolves . toEqual ( { lastUsedAt : null } ) ;
327+ } ) ;
328+
280329 postgresTest ( "treats empty stored scopes as restricted and deny-all" , async ( { prisma } ) => {
281330 const { organization, project, orgMember, user } = await createTestOrgProjectWithMember ( prisma ) ;
282331 const rbac = makeController ( prisma ) ;
@@ -481,6 +530,27 @@ describe("RBAC fallback — additional key permissions", () => {
481530} ) ;
482531
483532describe ( "RBAC fallback — branch header guards" , ( ) => {
533+ postgresTest ( "preview environments require a branch header" , async ( { prisma } ) => {
534+ const { organization, project } = await createTestOrgProjectWithMember ( prisma ) ;
535+ const rbac = makeController ( prisma ) ;
536+ const previewParent = await createEnv ( prisma , project . id , organization . id , {
537+ type : "PREVIEW" ,
538+ isBranchableEnvironment : true ,
539+ } ) ;
540+
541+ const result = await rbac . authenticateBearer ( bearerRequest ( previewParent . apiKey ) ) ;
542+
543+ expect ( result ) . toMatchObject ( {
544+ ok : false ,
545+ status : 401 ,
546+ error : "x-trigger-branch header required for preview env" ,
547+ resolution : {
548+ credentialKind : "root_api_key" ,
549+ lookupPath : "root_current" ,
550+ } ,
551+ } ) ;
552+ } ) ;
553+
484554 // The "default" sentinel is DEVELOPMENT-only: it maps the dev root env to its
485555 // (branchless) self. For PREVIEW, "default" is an ordinary branch name, so a
486556 // PREVIEW branch literally named "default" is reachable and the request pivots
0 commit comments