Skip to content

Commit a343f97

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(selectors): expose route verb to contract audit
1 parent b094718 commit a343f97

4 files changed

Lines changed: 84 additions & 12 deletions

File tree

apps/sim/app/api/selectors/execute/route.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ const mocks = vi.hoisted(() => ({ status: 200 }))
88

99
vi.mock('@/lib/api/server/routes', () => ({
1010
defineInternalJsonRoute: vi.fn(
11-
() => async () =>
11+
(options: { staticResponseHeaders?: HeadersInit }) => async () =>
1212
new Response(JSON.stringify({ ok: mocks.status < 400 }), {
1313
status: mocks.status,
14-
headers: { 'Content-Type': 'application/json' },
14+
headers: options.staticResponseHeaders,
1515
})
1616
),
1717
extendInternalErrorPolicy: vi.fn(() => ({})),

apps/sim/app/api/selectors/execute/route.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const selectorErrorPolicy = extendInternalErrorPolicy(internalOrchestrationError
3030
return null
3131
})
3232

33-
const executeSelectorRoute = defineInternalJsonRoute({
33+
export const POST = defineInternalJsonRoute({
3434
contract: executeSelectorContract,
3535
auth: internalSessionAuth,
3636
operation: selectorOperations.execute,
@@ -41,12 +41,5 @@ const executeSelectorRoute = defineInternalJsonRoute({
4141
parseOptions: { maxBodyBytes: 256 * 1024 },
4242
mapInput: ({ body }, { request }) => ({ ...body, signal: request.signal }),
4343
useCase: executeSelector,
44-
responseHeaders: () => PRIVATE_NO_STORE,
44+
staticResponseHeaders: PRIVATE_NO_STORE,
4545
})
46-
47-
/** Applies the privacy header to authentication, parse, and unhandled failures too. */
48-
export async function POST(...args: Parameters<typeof executeSelectorRoute>): Promise<Response> {
49-
const response = await executeSelectorRoute(...args)
50-
response.headers.set('Cache-Control', PRIVATE_NO_STORE['Cache-Control'])
51-
return response
52-
}

apps/sim/lib/api/server/routes/internal-json-route.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,75 @@ describe('defineInternalJsonRoute', () => {
215215
expect(body.requestId).toBe('req-parse')
216216
})
217217

218+
it('applies static response headers to success and every failure stage', async () => {
219+
const staticHeaderContract = defineRouteContract({
220+
method: 'POST',
221+
path: '/api/test/internal-json-route',
222+
body: z.object({ outcome: z.enum(['success', 'failure']) }),
223+
response: { mode: 'json', schema: z.object({ value: z.string() }) },
224+
})
225+
const handler = defineInternalJsonRoute({
226+
contract: staticHeaderContract,
227+
auth: {
228+
async authenticate(request) {
229+
if (request.headers.get('x-reject-auth') === 'true') {
230+
throw new InternalUnauthenticatedError('Unauthorized')
231+
}
232+
return { kind: 'session' as const, userId: 'user-1', sessionId: 'session-1' }
233+
},
234+
},
235+
operation,
236+
rateLimit: internalRateLimits.none({ reason: 'Unit test' }),
237+
errorPolicy: internalOrchestrationErrorPolicy,
238+
mapInput: ({ body }) => body.outcome,
239+
useCase: {
240+
operation,
241+
async execute({ input }) {
242+
if (input === 'failure') throw new Error('Unhandled')
243+
return { value: 'ok' }
244+
},
245+
},
246+
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
247+
})
248+
249+
const cases: Array<[NextRequest, number]> = [
250+
[
251+
new NextRequest('http://localhost/api/test/internal-json-route', {
252+
method: 'POST',
253+
body: JSON.stringify({ outcome: 'success' }),
254+
}),
255+
200,
256+
],
257+
[
258+
new NextRequest('http://localhost/api/test/internal-json-route', {
259+
method: 'POST',
260+
headers: { 'x-reject-auth': 'true' },
261+
}),
262+
401,
263+
],
264+
[
265+
new NextRequest('http://localhost/api/test/internal-json-route', {
266+
method: 'POST',
267+
body: '{',
268+
}),
269+
400,
270+
],
271+
[
272+
new NextRequest('http://localhost/api/test/internal-json-route', {
273+
method: 'POST',
274+
body: JSON.stringify({ outcome: 'failure' }),
275+
}),
276+
500,
277+
],
278+
]
279+
280+
for (const [request, expectedStatus] of cases) {
281+
const response = await handler(request)
282+
expect(response.status).toBe(expectedStatus)
283+
expect(response.headers.get('Cache-Control')).toBe('private, no-store')
284+
}
285+
})
286+
218287
it('orders auth, rate limiting, parsing, async mapping, and application execution', async () => {
219288
const events: string[] = []
220289
const orderedContract = defineRouteContract({

apps/sim/lib/api/server/routes/internal-json-route.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ type InternalJsonRouteOptions<
256256
}): void | Promise<void>
257257
onSuccess?(args: { principal: P; input: NoInfer<I>; result: NoInfer<R> }): void | Promise<void>
258258
statusForResult?(result: NoInfer<R>): number
259+
/** Headers applied last to every response path, including authentication and parse failures. */
260+
staticResponseHeaders?: HeadersInit
259261
responseHeaders?(args: { principal: P; input: NoInfer<I>; result: NoInfer<R> }): HeadersInit
260262
finalizeResponse?(args: {
261263
request: NextRequest
@@ -408,5 +410,13 @@ export function defineInternalJsonRoute<
408410
}
409411
)
410412

411-
return async (request, context) => wrapped(request, context)
413+
return async (request, context) => {
414+
const response = await wrapped(request, context)
415+
if (options.staticResponseHeaders) {
416+
new Headers(options.staticResponseHeaders).forEach((value, key) => {
417+
response.headers.set(key, value)
418+
})
419+
}
420+
return response
421+
}
412422
}

0 commit comments

Comments
 (0)