11/**
22 * @vitest -environment node
33 */
4- import { createMockRequest , hybridAuthMockFns } from '@sim/testing'
4+ import { createMockRequest } from '@sim/testing'
55import { NextResponse } from 'next/server'
66import { beforeEach , describe , expect , it , vi } from 'vitest'
7+ import { InternalUnauthenticatedError } from '@/lib/api/server/routes'
78import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
89import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
910
1011const {
1112 MockWindchillProviderError,
13+ mockAuthenticateWindchill,
1214 mockAssertToolFileAccess,
1315 mockCreateWindchillSession,
1416 mockDownloadServableFileFromStorage,
@@ -31,6 +33,7 @@ const {
3133
3234 return {
3335 MockWindchillProviderError,
36+ mockAuthenticateWindchill : vi . fn ( ) ,
3437 mockAssertToolFileAccess : vi . fn ( ) ,
3538 mockCreateWindchillSession : vi . fn ( ) ,
3639 mockDownloadServableFileFromStorage : vi . fn ( ) ,
@@ -43,6 +46,10 @@ const {
4346 }
4447} )
4548
49+ vi . mock ( '@/lib/windchill/api/route-policies' , ( ) => ( {
50+ internalWindchillExecutorAuth : { authenticate : mockAuthenticateWindchill } ,
51+ } ) )
52+
4653vi . mock ( '@/app/api/files/authorization' , ( ) => ( {
4754 assertToolFileAccess : mockAssertToolFileAccess ,
4855} ) )
@@ -166,7 +173,7 @@ const MUTATION_CASES = [
166173 } ,
167174 {
168175 operation : 'windchill_revise_documents' ,
169- input : { documentOids : [ DOCUMENT_OID ] , versionId : 'B' } ,
176+ input : { documentOids : [ DOCUMENT_OID ] } ,
170177 url : '/DocMgmt/ReviseDocuments' ,
171178 method : 'POST' ,
172179 } ,
@@ -186,12 +193,65 @@ const MUTATION_CASES = [
186193 } ,
187194] as const
188195
196+ const MUTATION_PAYLOAD_CASES = [
197+ {
198+ operation : 'windchill_check_out_documents' ,
199+ input : { documentOids : [ DOCUMENT_OID ] , checkOutNote : 'Editing' } ,
200+ body : { Documents : [ { ID : DOCUMENT_OID } ] , CheckOutNote : 'Editing' } ,
201+ } ,
202+ {
203+ operation : 'windchill_check_in_document' ,
204+ input : {
205+ documentOid : DOCUMENT_OID ,
206+ checkInNote : 'Done' ,
207+ keepCheckedOut : false ,
208+ checkOutNote : 'Continue editing' ,
209+ } ,
210+ body : {
211+ CheckInNote : 'Done' ,
212+ KeepCheckedOut : false ,
213+ CheckOutNote : 'Continue editing' ,
214+ } ,
215+ } ,
216+ {
217+ operation : 'windchill_revise_document' ,
218+ input : { documentOid : DOCUMENT_OID , versionId : 'B' } ,
219+ body : { VersionId : 'B' } ,
220+ } ,
221+ {
222+ operation : 'windchill_revise_documents' ,
223+ input : { documentOids : [ DOCUMENT_OID ] } ,
224+ body : { Documents : [ { ID : DOCUMENT_OID } ] } ,
225+ } ,
226+ {
227+ operation : 'windchill_set_lifecycle_state' ,
228+ input : { documentOid : DOCUMENT_OID , stateValue : 'RELEASED' , stateDisplay : 'Released' } ,
229+ body : { State : { Display : 'Released' , Value : 'RELEASED' } } ,
230+ } ,
231+ {
232+ operation : 'windchill_update_document_security_labels' ,
233+ input : {
234+ securityLabelUpdates : [ { id : DOCUMENT_OID , labels : { EXPORT_CONTROL : 'L1' } } ] ,
235+ } ,
236+ body : { Documents : [ { EXPORT_CONTROL : 'L1' , ID : DOCUMENT_OID } ] } ,
237+ } ,
238+ ] as const
239+
189240beforeEach ( ( ) => {
190241 vi . clearAllMocks ( )
191- hybridAuthMockFns . mockCheckInternalAuth . mockResolvedValue ( {
192- success : true ,
193- userId : 'user-1' ,
194- authType : 'internal_jwt' ,
242+ mockAuthenticateWindchill . mockResolvedValue ( {
243+ kind : 'delegated' ,
244+ serviceId : 'executor' ,
245+ subjectUserId : 'user-1' ,
246+ 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' ) ,
251+ delegationContext : {
252+ kind : 'workflow_execution' ,
253+ workflowId : '550e8400-e29b-41d4-a716-446655440001' ,
254+ } ,
195255 } )
196256 mockCreateWindchillSession . mockResolvedValue ( {
197257 nonceHeader : 'CSRF_NONCE' ,
@@ -230,15 +290,14 @@ beforeEach(() => {
230290
231291describe ( 'POST /api/tools/windchill' , ( ) => {
232292 it ( 'authenticates before parsing the request body' , async ( ) => {
233- hybridAuthMockFns . mockCheckInternalAuth . mockResolvedValueOnce ( {
234- success : false ,
235- error : 'Unauthorized' ,
236- } )
293+ mockAuthenticateWindchill . mockRejectedValueOnce (
294+ new InternalUnauthenticatedError ( 'Authentication required' )
295+ )
237296
238297 const response = await POST ( createMockRequest ( 'POST' , { operation : 'not-valid' } ) )
239298
240299 expect ( response . status ) . toBe ( 401 )
241- expect ( await response . json ( ) ) . toEqual ( { success : false , error : 'Unauthorized ' } )
300+ expect ( await response . json ( ) ) . toEqual ( { success : false , error : 'Authentication required ' } )
242301 expect ( mockCreateWindchillSession ) . not . toHaveBeenCalled ( )
243302 } )
244303
@@ -291,6 +350,22 @@ describe('POST /api/tools/windchill', () => {
291350 }
292351 )
293352
353+ it . each ( MUTATION_PAYLOAD_CASES ) (
354+ 'encodes the exact $operation action payload' ,
355+ async ( { operation, input, body } ) => {
356+ const response = await POST (
357+ createMockRequest ( 'POST' , {
358+ ...BASE_BODY ,
359+ operation,
360+ ...input ,
361+ } )
362+ )
363+
364+ expect ( response . status ) . toBe ( 200 )
365+ expect ( mockWindchillMutationRequest . mock . calls [ 0 ] [ 0 ] . body ) . toEqual ( body )
366+ }
367+ )
368+
294369 it ( 'maps create bindings and custom attributes without allowing them to replace bindings' , async ( ) => {
295370 const response = await POST (
296371 createMockRequest ( 'POST' , {
@@ -594,7 +669,22 @@ describe('POST /api/tools/windchill', () => {
594669 )
595670 } )
596671
597- it ( 'uses execution storage when a complete execution context is provided' , async ( ) => {
672+ it ( 'uses execution storage derived from the bound delegation principal' , async ( ) => {
673+ mockAuthenticateWindchill . mockResolvedValueOnce ( {
674+ kind : 'delegated' ,
675+ serviceId : 'executor' ,
676+ subjectUserId : 'user-1' ,
677+ workspaceId : '550e8400-e29b-41d4-a716-446655440000' ,
678+ delegationId : 'delegation-1' ,
679+ audience : 'sim:windchill' ,
680+ issuedAt : new Date ( '2026-01-01T00:00:00.000Z' ) ,
681+ expiresAt : new Date ( '2027-01-01T00:00:00.000Z' ) ,
682+ delegationContext : {
683+ kind : 'workflow_execution' ,
684+ workflowId : '550e8400-e29b-41d4-a716-446655440001' ,
685+ executionId : 'execution-1' ,
686+ } ,
687+ } )
598688 mockUploadExecutionFile . mockResolvedValueOnce ( {
599689 id : 'file-2' ,
600690 name : 'specification.pdf' ,
@@ -609,14 +699,24 @@ describe('POST /api/tools/windchill', () => {
609699 ...BASE_BODY ,
610700 operation : 'windchill_download_primary_content' ,
611701 documentOid : DOCUMENT_OID ,
612- workspaceId : '550e8400-e29b-41d4-a716-446655440000 ' ,
613- workflowId : '550e8400-e29b-41d4-a716-446655440001 ' ,
614- executionId : 'execution-1 ' ,
702+ workspaceId : 'forged-workspace ' ,
703+ workflowId : 'forged-workflow ' ,
704+ executionId : 'forged-execution ' ,
615705 } )
616706 )
617707
618708 expect ( response . status ) . toBe ( 200 )
619- expect ( mockUploadExecutionFile ) . toHaveBeenCalledTimes ( 1 )
709+ expect ( mockUploadExecutionFile ) . toHaveBeenCalledWith (
710+ {
711+ workspaceId : '550e8400-e29b-41d4-a716-446655440000' ,
712+ workflowId : '550e8400-e29b-41d4-a716-446655440001' ,
713+ executionId : 'execution-1' ,
714+ } ,
715+ Buffer . from ( 'pdf' ) ,
716+ 'specification.pdf' ,
717+ 'application/pdf' ,
718+ 'user-1'
719+ )
620720 expect ( mockUploadCopilotFile ) . not . toHaveBeenCalled ( )
621721 } )
622722
0 commit comments