Skip to content

Commit 2c504d2

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(windchill): align execution and API contracts
1 parent 4c8a60a commit 2c504d2

17 files changed

Lines changed: 877 additions & 200 deletions

File tree

apps/docs/content/docs/en/integrations/windchill.mdx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ Create multiple WT.Document objects atomically
321321

322322
### Windchill Update Document
323323

324-
Update attributes on one document
324+
Update PATCH-compatible attributes on one document
325325

326326
#### Input
327327

@@ -331,7 +331,7 @@ Update attributes on one document
331331
| `username` | string | Yes | Windchill service-account username |
332332
| `password` | string | Yes | Windchill service-account password |
333333
| `documentOid` | string | Yes | WT.Document OID, for example OR:wt.doc.WTDocument:48796581 |
334-
| `attributes` | json | Yes | Installed Windchill document attributes to update as a non-empty JSON object |
334+
| `attributes` | json | Yes | Installed PATCH-compatible attributes as bounded JSON. Name, Number, and Organization require UpdateCommonProperties and are not supported here. |
335335

336336
#### Output
337337

@@ -357,7 +357,7 @@ Update attributes on one document
357357

358358
### Windchill Update Documents
359359

360-
Update multiple documents atomically
360+
Update PATCH-compatible attributes on multiple documents atomically
361361

362362
#### Input
363363

@@ -366,7 +366,7 @@ Update multiple documents atomically
366366
| `baseUrl` | string | Yes | Complete WRS 2.7 versioned service root using Basic authentication, for example https://host/Windchill/servlet/odata/v6 |
367367
| `username` | string | Yes | Windchill service-account username |
368368
| `password` | string | Yes | Windchill service-account password |
369-
| `documents` | json | Yes | Document updates as a JSON array; each item requires id and non-empty attributes |
369+
| `documents` | json | Yes | Document updates as a JSON array; each item requires id and bounded PATCH-compatible attributes. Name, Number, and Organization are not supported. |
370370

371371
#### Output
372372

@@ -696,7 +696,6 @@ Create new revisions of multiple documents atomically
696696
| `username` | string | Yes | Windchill service-account username |
697697
| `password` | string | Yes | Windchill service-account password |
698698
| `documentOids` | array | Yes | WT.Document OIDs to process atomically |
699-
| `versionId` | string | No | Optional target revision identifier when override-on-revise is enabled |
700699

701700
#### Output
702701

apps/sim/app/api/tools/windchill/route.test.ts

Lines changed: 116 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
/**
22
* @vitest-environment node
33
*/
4-
import { createMockRequest, hybridAuthMockFns } from '@sim/testing'
4+
import { createMockRequest } from '@sim/testing'
55
import { NextResponse } from 'next/server'
66
import { beforeEach, describe, expect, it, vi } from 'vitest'
7+
import { InternalUnauthenticatedError } from '@/lib/api/server/routes'
78
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
89
import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
910

1011
const {
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+
4653
vi.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+
189240
beforeEach(() => {
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

231291
describe('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

Comments
 (0)