11import { beforeEach , describe , expect , it , vi } from "vitest" ;
22
33const rbacMocks = vi . hoisted ( ( ) => ( {
4- authenticateAuthorizeBearer : vi . fn < ( ...args : any [ ] ) => Promise < any > > ( ) ,
4+ authenticateBearer : vi . fn < ( ...args : any [ ] ) => Promise < any > > ( ) ,
55} ) ) ;
66
7+ const telemetryMocks = vi . hoisted ( ( ) => ( {
8+ attemptsAdd : vi . fn ( ) ,
9+ durationRecord : vi . fn ( ) ,
10+ } ) ) ;
11+
12+ vi . mock ( "@internal/tracing" , ( ) => ( {
13+ getMeter : ( ) => ( {
14+ createCounter : ( ) => ( { add : telemetryMocks . attemptsAdd } ) ,
15+ createHistogram : ( ) => ( { record : telemetryMocks . durationRecord } ) ,
16+ createObservableGauge : ( ) => ( { addCallback : vi . fn ( ) } ) ,
17+ } ) ,
18+ } ) ) ;
719vi . mock ( "~/services/rbac.server" , ( ) => ( { rbac : rbacMocks } ) ) ;
820vi . mock ( "~/db.server" , ( ) => ( { prisma : { } , $replica : { } } ) ) ;
921vi . mock ( "~/env.server" , ( ) => ( { env : { SESSION_SECRET : "test-session-secret" } } ) ) ;
@@ -35,7 +47,9 @@ import { authenticateApiKeyWithScope } from "~/services/apiAuth.server";
3547
3648describe ( "authenticateApiKeyWithScope" , ( ) => {
3749 beforeEach ( ( ) => {
38- rbacMocks . authenticateAuthorizeBearer . mockReset ( ) ;
50+ rbacMocks . authenticateBearer . mockReset ( ) ;
51+ telemetryMocks . attemptsAdd . mockReset ( ) ;
52+ telemetryMocks . durationRecord . mockReset ( ) ;
3953 } ) ;
4054
4155 it ( "returns 401 without a bearer credential" , async ( ) => {
@@ -49,14 +63,14 @@ describe("authenticateApiKeyWithScope", () => {
4963 status : 401 ,
5064 error : "Invalid or Missing API key" ,
5165 } ) ;
52- expect ( rbacMocks . authenticateAuthorizeBearer ) . not . toHaveBeenCalled ( ) ;
66+ expect ( rbacMocks . authenticateBearer ) . not . toHaveBeenCalled ( ) ;
5367 } ) ;
5468
5569 it . each ( [
5670 { status : 401 as const , error : "Invalid API key" } ,
5771 { status : 403 as const , error : "Unauthorized" } ,
5872 ] ) ( "preserves controller $status failures" , async ( failure ) => {
59- rbacMocks . authenticateAuthorizeBearer . mockResolvedValue ( { ok : false , ...failure } ) ;
73+ rbacMocks . authenticateBearer . mockResolvedValue ( { ok : false , ...failure } ) ;
6074 const request = new Request ( "https://example.com" , {
6175 headers : { Authorization : "Bearer tr_test_key" } ,
6276 } ) ;
@@ -72,11 +86,12 @@ describe("authenticateApiKeyWithScope", () => {
7286 it ( "bridges controller success into the legacy private authentication shape" , async ( ) => {
7387 const environment = { id : "env_123" } ;
7488 const ability = { can : vi . fn ( ( ) => true ) , canSuper : vi . fn ( ( ) => true ) } ;
75- rbacMocks . authenticateAuthorizeBearer . mockResolvedValue ( {
89+ rbacMocks . authenticateBearer . mockResolvedValue ( {
7690 ok : true ,
7791 environment,
7892 ability,
7993 subject : { type : "apiKey" , apiKeyId : "key_123" } ,
94+ resolution : { credentialKind : "root_api_key" , lookupPath : "root_current" } ,
8095 } ) ;
8196 const request = new Request ( "https://example.com" , {
8297 headers : { Authorization : "Bearer tr_test_key" , "x-trigger-branch" : "feature/test" } ,
@@ -88,10 +103,22 @@ describe("authenticateApiKeyWithScope", () => {
88103 allowJWT : true ,
89104 } ) ;
90105
91- expect ( rbacMocks . authenticateAuthorizeBearer ) . toHaveBeenCalledWith (
92- request ,
93- { action : "read" , resource : { type : "envvars" } } ,
94- { allowJWT : true }
106+ expect ( rbacMocks . authenticateBearer ) . toHaveBeenCalledWith ( request , { allowJWT : true } ) ;
107+ expect ( ability . can ) . toHaveBeenCalledWith ( "read" , { type : "envvars" } ) ;
108+ expect ( telemetryMocks . attemptsAdd ) . toHaveBeenCalledWith ( 1 , {
109+ resolver : "rbac" ,
110+ credential_kind : "root_api_key" ,
111+ result : "success" ,
112+ lookup_path : "root_current" ,
113+ } ) ;
114+ expect ( telemetryMocks . durationRecord ) . toHaveBeenCalledWith (
115+ expect . any ( Number ) ,
116+ expect . objectContaining ( {
117+ resolver : "rbac" ,
118+ credential_kind : "root_api_key" ,
119+ result : "success" ,
120+ lookup_path : "root_current" ,
121+ } )
95122 ) ;
96123 expect ( result ) . toEqual ( {
97124 ok : true ,
@@ -104,4 +131,33 @@ describe("authenticateApiKeyWithScope", () => {
104131 } ,
105132 } ) ;
106133 } ) ;
134+
135+ it ( "records successful authentication before returning an authorization failure" , async ( ) => {
136+ const ability = { can : vi . fn ( ( ) => false ) , canSuper : vi . fn ( ( ) => false ) } ;
137+ rbacMocks . authenticateBearer . mockResolvedValue ( {
138+ ok : true ,
139+ environment : { id : "env_123" } ,
140+ ability,
141+ subject : { type : "apiKey" , apiKeyId : "key_123" } ,
142+ resolution : { credentialKind : "additional_api_key" , lookupPath : "additional" } ,
143+ } ) ;
144+ const request = new Request ( "https://example.com" , {
145+ headers : { Authorization : "Bearer tr_prod_sk_0123456789abcdefghijklmn" } ,
146+ } ) ;
147+
148+ await expect (
149+ authenticateApiKeyWithScope ( request , {
150+ action : "write" ,
151+ resource : { type : "deployments" } ,
152+ } )
153+ ) . resolves . toEqual ( { ok : false , status : 403 , error : "Unauthorized" } ) ;
154+
155+ expect ( ability . can ) . toHaveBeenCalledWith ( "write" , { type : "deployments" } ) ;
156+ expect ( telemetryMocks . attemptsAdd ) . toHaveBeenCalledWith ( 1 , {
157+ resolver : "rbac" ,
158+ credential_kind : "additional_api_key" ,
159+ result : "success" ,
160+ lookup_path : "additional" ,
161+ } ) ;
162+ } ) ;
107163} ) ;
0 commit comments