@@ -14,6 +14,8 @@ import {
1414 IssuanceV2 ,
1515 UnitV2 ,
1616 LocationV2 ,
17+ StakeholderV2 ,
18+ StakeholderProjectV2 ,
1719} from '../../../src/models/v2/index.js' ;
1820import { Staging } from '../../../src/models/index.js' ;
1921import TaskManager from '../../../src/tasks/index.js' ;
@@ -1002,6 +1004,124 @@ describe('V2 Staging Integration Tests', function () {
10021004 expect ( parsedData [ 0 ] . cad_trust_project_id ) . to . equal ( projectId ) ;
10031005 } ) ;
10041006
1007+ it ( 'should allow editRecord for a join table update that changes only the non-owning reference' , async function ( ) {
1008+ const homeOrgId = await getV2HomeOrgId ( ) ;
1009+ const stakeholder = await StakeholderV2 . create ( {
1010+ cadTrustStakeholderId : uuidv4 ( ) ,
1011+ stakeholderName : 'Home Stakeholder' ,
1012+ stakeholderType : 'Owner' ,
1013+ orgUid : homeOrgId ,
1014+ } ) ;
1015+ // Replacement stakeholder is owned by another org on purpose: ownership for
1016+ // this join table is anchored to the project, so swapping the stakeholder
1017+ // cross-reference must be allowed even when it points at a foreign stakeholder.
1018+ const replacementStakeholder = await StakeholderV2 . create ( {
1019+ cadTrustStakeholderId : uuidv4 ( ) ,
1020+ stakeholderName : 'Replacement Foreign Stakeholder' ,
1021+ stakeholderType : 'Owner' ,
1022+ orgUid : 'other-org-uid-12345' ,
1023+ } ) ;
1024+ const joinRecord = await StakeholderProjectV2 . create ( {
1025+ cadTrustStakeholderProjectId : uuidv4 ( ) ,
1026+ cadTrustStakeholderId : stakeholder . cadTrustStakeholderId ,
1027+ cadTrustProjectId : projectId ,
1028+ } ) ;
1029+
1030+ const stagingUuid = uuidv4 ( ) ;
1031+ await StagingV2 . create ( {
1032+ uuid : stagingUuid ,
1033+ table : 'stakeholder_projects' ,
1034+ action : 'UPDATE' ,
1035+ data : JSON . stringify ( [ {
1036+ cad_trust_stakeholder_project_id : joinRecord . cadTrustStakeholderProjectId ,
1037+ cad_trust_project_id : projectId ,
1038+ cad_trust_stakeholder_id : stakeholder . cadTrustStakeholderId ,
1039+ } ] ) ,
1040+ committed : false ,
1041+ failed_commit : false ,
1042+ is_transfer : false ,
1043+ } ) ;
1044+
1045+ // Ownership for the join table is resolved through the project (owning side),
1046+ // so editing only the stakeholder cross-reference must not be rejected.
1047+ await supertest ( app )
1048+ . put ( '/v2/staging' )
1049+ . send ( {
1050+ uuid : stagingUuid ,
1051+ data : [ {
1052+ cad_trust_stakeholder_project_id : joinRecord . cadTrustStakeholderProjectId ,
1053+ cad_trust_stakeholder_id : replacementStakeholder . cadTrustStakeholderId ,
1054+ } ] ,
1055+ } )
1056+ . expect ( 200 ) ;
1057+
1058+ const updatedRecord = await StagingV2 . findOne ( {
1059+ where : { uuid : stagingUuid } ,
1060+ } ) ;
1061+ const parsedData = JSON . parse ( updatedRecord . data ) ;
1062+ expect ( parsedData [ 0 ] . cad_trust_stakeholder_id ) . to . equal ( replacementStakeholder . cadTrustStakeholderId ) ;
1063+ } ) ;
1064+
1065+ it ( 'should reject editRecord that retargets a join table to another organization owning record' , async function ( ) {
1066+ const homeOrgId = await getV2HomeOrgId ( ) ;
1067+ const stakeholder = await StakeholderV2 . create ( {
1068+ cadTrustStakeholderId : uuidv4 ( ) ,
1069+ stakeholderName : 'Home Stakeholder For Retarget' ,
1070+ stakeholderType : 'Owner' ,
1071+ orgUid : homeOrgId ,
1072+ } ) ;
1073+ const foreignProject = await ProjectV2 . create ( {
1074+ cadTrustProjectId : uuidv4 ( ) ,
1075+ orgUid : 'other-org-uid-12345' ,
1076+ projectRegistryName : 'Other Registry' ,
1077+ projectId : 'OTHER-JOIN-RETARGET-001' ,
1078+ projectName : 'Other Org Project' ,
1079+ } ) ;
1080+ const joinRecord = await StakeholderProjectV2 . create ( {
1081+ cadTrustStakeholderProjectId : uuidv4 ( ) ,
1082+ cadTrustStakeholderId : stakeholder . cadTrustStakeholderId ,
1083+ cadTrustProjectId : projectId ,
1084+ } ) ;
1085+
1086+ const stagingUuid = uuidv4 ( ) ;
1087+ await StagingV2 . create ( {
1088+ uuid : stagingUuid ,
1089+ table : 'stakeholder_projects' ,
1090+ action : 'UPDATE' ,
1091+ data : JSON . stringify ( [ {
1092+ cad_trust_stakeholder_project_id : joinRecord . cadTrustStakeholderProjectId ,
1093+ cad_trust_project_id : projectId ,
1094+ cad_trust_stakeholder_id : stakeholder . cadTrustStakeholderId ,
1095+ } ] ) ,
1096+ committed : false ,
1097+ failed_commit : false ,
1098+ is_transfer : false ,
1099+ } ) ;
1100+
1101+ // Retargeting the owning project FK to another org's project must be rejected,
1102+ // since ownership for this join table is anchored to the project.
1103+ const response = await supertest ( app )
1104+ . put ( '/v2/staging' )
1105+ . send ( {
1106+ uuid : stagingUuid ,
1107+ data : [ {
1108+ cad_trust_stakeholder_project_id : joinRecord . cadTrustStakeholderProjectId ,
1109+ cad_trust_project_id : foreignProject . cadTrustProjectId ,
1110+ cad_trust_stakeholder_id : stakeholder . cadTrustStakeholderId ,
1111+ } ] ,
1112+ } )
1113+ . expect ( 400 ) ;
1114+
1115+ expect ( response . body . success ) . to . be . false ;
1116+ expect ( response . body . error ) . to . include ( 'Restricted data' ) ;
1117+
1118+ const unchangedRecord = await StagingV2 . findOne ( {
1119+ where : { uuid : stagingUuid } ,
1120+ } ) ;
1121+ const parsedData = JSON . parse ( unchangedRecord . data ) ;
1122+ expect ( parsedData [ 0 ] . cad_trust_project_id ) . to . equal ( projectId ) ;
1123+ } ) ;
1124+
10051125 it ( 'should reject editRecord when data is not an array (DoS prevention)' , async function ( ) {
10061126 const programData = await generateV2ProgramData ( ) ;
10071127 const stagingUuid = uuidv4 ( ) ;
0 commit comments