11/**
22 * @vitest -environment node
33 */
4- import { createMockRequest } from '@sim/testing'
4+ import { createMockRequest as createTestingRequest , resetEnvMock } from '@sim/testing'
55import { NextResponse } from 'next/server'
6- import { beforeEach , describe , expect , it , vi } from 'vitest'
7- import { InternalUnauthenticatedError } from '@/lib/api/server/routes'
6+ import { afterAll , beforeAll , beforeEach , describe , expect , it , vi } from 'vitest'
87import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
98import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
109
1110const {
11+ MockInvalidBindingError,
1212 MockWindchillProviderError,
13- mockAuthenticateWindchill,
1413 mockAssertToolFileAccess,
14+ mockBindDelegation,
1515 mockCreateWindchillSession,
1616 mockDownloadServableFileFromStorage,
1717 mockDownloadWindchillContent,
18+ mockGetSession,
1819 mockProcessFilesToUserFiles,
1920 mockUploadCopilotFile,
2021 mockUploadExecutionFile,
2122 mockUploadWindchillContent,
2223 mockWindchillMutationRequest,
2324} = vi . hoisted ( ( ) => {
25+ class MockInvalidBindingError extends Error { }
2426 class MockWindchillProviderError extends Error {
2527 constructor (
2628 message : string ,
@@ -32,12 +34,14 @@ const {
3234 }
3335
3436 return {
37+ MockInvalidBindingError,
3538 MockWindchillProviderError,
36- mockAuthenticateWindchill : vi . fn ( ) ,
3739 mockAssertToolFileAccess : vi . fn ( ) ,
40+ mockBindDelegation : vi . fn ( ) ,
3841 mockCreateWindchillSession : vi . fn ( ) ,
3942 mockDownloadServableFileFromStorage : vi . fn ( ) ,
4043 mockDownloadWindchillContent : vi . fn ( ) ,
44+ mockGetSession : vi . fn ( ) ,
4145 mockProcessFilesToUserFiles : vi . fn ( ) ,
4246 mockUploadCopilotFile : vi . fn ( ) ,
4347 mockUploadExecutionFile : vi . fn ( ) ,
@@ -46,9 +50,12 @@ const {
4650 }
4751} )
4852
49- vi . mock ( '@/lib/windchill/api/route-policies' , ( ) => ( {
50- internalWindchillExecutorAuth : { authenticate : mockAuthenticateWindchill } ,
53+ vi . mock ( '@/lib/auth' , ( ) => ( { getSession : mockGetSession } ) )
54+ vi . mock ( '@/lib/auth/internal-delegation' , ( ) => ( {
55+ bindInternalExecutorDelegation : mockBindDelegation ,
56+ InvalidInternalDelegationBindingError : MockInvalidBindingError ,
5157} ) )
58+ vi . unmock ( '@/lib/auth/internal' )
5259
5360vi . mock ( '@/app/api/files/authorization' , ( ) => ( {
5461 assertToolFileAccess : mockAssertToolFileAccess ,
@@ -79,6 +86,7 @@ vi.mock('@/tools/windchill/utils.server', () => ({
7986 WindchillProviderError : MockWindchillProviderError ,
8087} ) )
8188
89+ import { generateInternalDelegationToken , generateInternalToken } from '@/lib/auth/internal'
8290import { POST } from '@/app/api/tools/windchill/route'
8391
8492const BASE_BODY = {
@@ -89,6 +97,15 @@ const BASE_BODY = {
8997
9098const DOCUMENT_OID = 'OR:wt.doc.WTDocument:1'
9199const SECOND_DOCUMENT_OID = 'OR:wt.doc.WTDocument:2'
100+ let delegationToken = ''
101+ let legacyInternalToken = ''
102+
103+ function createMockRequest ( method : string , body : unknown , headers : Record < string , string > = { } ) {
104+ return createTestingRequest ( method , body , {
105+ authorization : `Bearer ${ delegationToken } ` ,
106+ ...headers ,
107+ } )
108+ }
92109
93110const MUTATION_CASES = [
94111 {
@@ -237,22 +254,34 @@ const MUTATION_PAYLOAD_CASES = [
237254 } ,
238255] as const
239256
257+ beforeAll ( async ( ) => {
258+ delegationToken = await generateInternalDelegationToken ( {
259+ subjectUserId : 'user-1' ,
260+ workflowId : '550e8400-e29b-41d4-a716-446655440001' ,
261+ } )
262+ legacyInternalToken = await generateInternalToken ( )
263+ } )
264+
265+ afterAll ( resetEnvMock )
266+
240267beforeEach ( ( ) => {
241268 vi . clearAllMocks ( )
242- mockAuthenticateWindchill . mockResolvedValue ( {
269+ mockGetSession . mockResolvedValue ( null )
270+ mockBindDelegation . mockImplementation ( async ( delegation , options ) => ( {
243271 kind : 'delegated' ,
244272 serviceId : 'executor' ,
245- subjectUserId : 'user-1' ,
273+ subjectUserId : delegation . subjectUserId ,
246274 workspaceId : '550e8400-e29b-41d4-a716-446655440000' ,
247- delegationId : ' delegation-1' ,
248- audience : 'sim:windchill' ,
249- issuedAt : new Date ( '2026-01-01T00:00:00.000Z' ) ,
250- expiresAt : new Date ( '2027-01-01T00:00:00.000Z' ) ,
275+ delegationId : delegation . delegationId ,
276+ audience : options . audience ,
277+ issuedAt : delegation . issuedAt ,
278+ expiresAt : delegation . expiresAt ,
251279 delegationContext : {
252280 kind : 'workflow_execution' ,
253- workflowId : '550e8400-e29b-41d4-a716-446655440001' ,
281+ workflowId : delegation . workflowId ,
282+ executionId : delegation . executionId ,
254283 } ,
255- } )
284+ } ) )
256285 mockCreateWindchillSession . mockResolvedValue ( {
257286 nonceHeader : 'CSRF_NONCE' ,
258287 nonceValue : 'nonce-value' ,
@@ -290,17 +319,48 @@ beforeEach(() => {
290319
291320describe ( 'POST /api/tools/windchill' , ( ) => {
292321 it ( 'authenticates before parsing the request body' , async ( ) => {
293- mockAuthenticateWindchill . mockRejectedValueOnce (
294- new InternalUnauthenticatedError ( 'Authentication required' )
295- )
296-
297- const response = await POST ( createMockRequest ( 'POST' , { operation : 'not-valid' } ) )
322+ const response = await POST ( createTestingRequest ( 'POST' , { operation : 'not-valid' } ) )
298323
299324 expect ( response . status ) . toBe ( 401 )
300- expect ( await response . json ( ) ) . toEqual ( { success : false , error : 'Authentication required ' } )
325+ expect ( await response . json ( ) ) . toEqual ( { success : false , error : 'Unauthorized ' } )
301326 expect ( mockCreateWindchillSession ) . not . toHaveBeenCalled ( )
302327 } )
303328
329+ it ( 'binds executor identity and scope through the canonical delegation path' , async ( ) => {
330+ const response = await POST (
331+ createMockRequest ( 'POST' , {
332+ ...BASE_BODY ,
333+ operation : 'windchill_update_document' ,
334+ documentOid : DOCUMENT_OID ,
335+ attributes : { Title : 'Updated' } ,
336+ } )
337+ )
338+
339+ expect ( response . status ) . toBe ( 200 )
340+ expect ( mockBindDelegation ) . toHaveBeenCalledWith ( expect . any ( Object ) , {
341+ audience : 'sim:windchill' ,
342+ resourceScope : undefined ,
343+ } )
344+ } )
345+
346+ it ( 'rejects browser sessions and legacy internal tokens' , async ( ) => {
347+ mockGetSession . mockResolvedValueOnce ( {
348+ user : { id : 'user-1' } ,
349+ session : { id : 'session-1' } ,
350+ } )
351+
352+ const sessionResponse = await POST ( createTestingRequest ( 'POST' , BASE_BODY ) )
353+ const legacyResponse = await POST (
354+ createTestingRequest ( 'POST' , BASE_BODY , {
355+ authorization : `Bearer ${ legacyInternalToken } ` ,
356+ } )
357+ )
358+
359+ expect ( sessionResponse . status ) . toBe ( 401 )
360+ expect ( legacyResponse . status ) . toBe ( 401 )
361+ expect ( mockBindDelegation ) . not . toHaveBeenCalled ( )
362+ } )
363+
304364 it ( 'rejects malformed operation inputs at the shared contract boundary' , async ( ) => {
305365 const response = await POST (
306366 createMockRequest ( 'POST' , {
@@ -670,7 +730,7 @@ describe('POST /api/tools/windchill', () => {
670730 } )
671731
672732 it ( 'uses execution storage derived from the bound delegation principal' , async ( ) => {
673- mockAuthenticateWindchill . mockResolvedValueOnce ( {
733+ mockBindDelegation . mockResolvedValueOnce ( {
674734 kind : 'delegated' ,
675735 serviceId : 'executor' ,
676736 subjectUserId : 'user-1' ,
0 commit comments