@@ -19,6 +19,7 @@ import { NextRequest } from "next/server";
1919import { afterEach , describe , expect , it , vi } from "vitest" ;
2020
2121import { responseBodyForStatus } from "../proxy-response" ;
22+ import { pickBackendBearer } from "@/shared/auth/session" ;
2223
2324vi . mock ( "@/lib/env" , ( ) => ( {
2425 serverEnv : {
@@ -41,6 +42,9 @@ const ctx = { params: Promise.resolve({ path: ["roles", "role-1", "privileges"]
4142
4243afterEach ( ( ) => {
4344 fetchMock . mockReset ( ) ;
45+ vi . mocked ( pickBackendBearer ) . mockReturnValue ( "access-token-abc" ) ;
46+ vi . unstubAllEnvs ( ) ;
47+ vi . restoreAllMocks ( ) ;
4448} ) ;
4549
4650describe ( "responseBodyForStatus" , ( ) => {
@@ -117,4 +121,65 @@ describe("api v1 proxy route", () => {
117121 "https://core.example.org/roles/role-1/privileges" ,
118122 ) ;
119123 } ) ;
124+
125+ it ( "rejects unauthenticated requests before contacting an upstream" , async ( ) => {
126+ vi . mocked ( pickBackendBearer ) . mockReturnValueOnce ( null ) ;
127+
128+ const response = await GET (
129+ new NextRequest ( "http://localhost:3000/api/v1/signer/admin/certificates" ) ,
130+ { params : Promise . resolve ( { path : [ "signer" , "admin" , "certificates" ] } ) } ,
131+ ) ;
132+
133+ expect ( response . status ) . toBe ( 401 ) ;
134+ expect ( await response . json ( ) ) . toEqual ( {
135+ code : "missing_bearer" ,
136+ message : "Not authenticated" ,
137+ } ) ;
138+ expect ( fetchMock ) . not . toHaveBeenCalled ( ) ;
139+ } ) ;
140+
141+ it ( "returns a sanitized 503 when the selected upstream is unavailable" , async ( ) => {
142+ const consoleError = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => undefined ) ;
143+ fetchMock . mockRejectedValueOnce ( new TypeError ( "fetch failed for secret upstream URL" ) ) ;
144+
145+ const response = await GET (
146+ new NextRequest ( "http://localhost:3000/api/v1/signer/admin/certificates?limit=20" ) ,
147+ { params : Promise . resolve ( { path : [ "signer" , "admin" , "certificates" ] } ) } ,
148+ ) ;
149+
150+ expect ( response . status ) . toBe ( 503 ) ;
151+ expect ( await response . json ( ) ) . toEqual ( {
152+ code : "upstream_unavailable" ,
153+ message : "Backend service is unavailable" ,
154+ } ) ;
155+ expect ( consoleError ) . toHaveBeenCalledWith ( "API proxy upstream unavailable" , {
156+ service : "signer" ,
157+ method : "GET" ,
158+ path : "/api/v1/admin/certificates" ,
159+ } ) ;
160+ expect ( JSON . stringify ( consoleError . mock . calls ) ) . not . toContain ( "access-token-abc" ) ;
161+ expect ( JSON . stringify ( consoleError . mock . calls ) ) . not . toContain ( "signer.example.org" ) ;
162+ expect ( JSON . stringify ( consoleError . mock . calls ) ) . not . toContain ( "secret upstream URL" ) ;
163+ } ) ;
164+
165+ it ( "fails explicitly if a hermetic signer E2E request reaches the proxy" , async ( ) => {
166+ vi . stubEnv ( "CUSTOS_E2E_FAIL_ON_SIGNER_PROXY_REQUEST" , "true" ) ;
167+ const consoleError = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => undefined ) ;
168+
169+ const response = await GET (
170+ new NextRequest ( "http://localhost:3000/api/v1/signer/admin/certificates" ) ,
171+ { params : Promise . resolve ( { path : [ "signer" , "admin" , "certificates" ] } ) } ,
172+ ) ;
173+
174+ expect ( response . status ) . toBe ( 500 ) ;
175+ expect ( await response . json ( ) ) . toEqual ( {
176+ code : "unexpected_signer_proxy_request" ,
177+ message : "Signer E2E request escaped its Playwright route" ,
178+ } ) ;
179+ expect ( fetchMock ) . not . toHaveBeenCalled ( ) ;
180+ expect ( consoleError ) . toHaveBeenCalledWith (
181+ "Unexpected signer proxy request during hermetic E2E" ,
182+ { method : "GET" , path : "/api/v1/signer/admin/certificates" } ,
183+ ) ;
184+ } ) ;
120185} ) ;
0 commit comments