@@ -14,6 +14,7 @@ const { BackbeatRoutesClient, PutDataCommand, VersionIdCollisionException } = re
1414const { generateVersionId, encode : encodeVersionId } = versioning . VersionID ;
1515
1616const TEST_BUCKET = `bucket-putdata-${ uuidv4 ( ) . split ( '-' ) [ 0 ] } ` ;
17+ const TEST_BUCKET_UNVERSIONED = `bucket-putdata-unver-${ uuidv4 ( ) . split ( '-' ) [ 0 ] } ` ;
1718const OBJECT_BODY = 'imAboutToBeCascadedWitNoParachuteInMyBack' ;
1819const OBJECT_MD5_HEX = createHash ( 'md5' ) . update ( OBJECT_BODY ) . digest ( 'hex' ) ;
1920const CANONICAL_ID = '79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be' ;
@@ -55,6 +56,8 @@ before(async () => {
5556 VersioningConfiguration : { Status : 'Enabled' } ,
5657 } ) ,
5758 ) ;
59+
60+ await s3 . send ( new CreateBucketCommand ( { Bucket : TEST_BUCKET_UNVERSIONED } ) ) ;
5861} ) ;
5962
6063describe ( 'putData : VersionId collision detection' , ( ) => {
@@ -106,17 +109,63 @@ describe('putData : VersionId collision detection', () => {
106109 const output = await putData ( key , { versionId : differentVersionId } ) ;
107110 assert . ok ( output . Location , 'should return a Location when data is written normally' ) ;
108111 } ) ;
112+
113+ it ( 'should throw VersionIdCollisionException when a non-current version already exists' , async ( ) => {
114+ const key = 'putdata-non-current-collision' ;
115+
116+ const v1Result = await s3 . send (
117+ new PutObjectCommand ( {
118+ Bucket : TEST_BUCKET ,
119+ Key : key ,
120+ Body : Buffer . from ( OBJECT_BODY ) ,
121+ ContentType : 'text/plain' ,
122+ } ) ,
123+ ) ;
124+ const v1VersionId = v1Result . VersionId ;
125+ assert . ok ( v1VersionId , 'first PutObject should return a VersionId' ) ;
126+
127+ const v2Result = await s3 . send (
128+ new PutObjectCommand ( {
129+ Bucket : TEST_BUCKET ,
130+ Key : key ,
131+ Body : Buffer . from ( OBJECT_BODY ) ,
132+ ContentType : 'text/plain' ,
133+ } ) ,
134+ ) ;
135+ assert . ok ( v2Result . VersionId , 'second PutObject should return a VersionId' ) ;
136+
137+ // putData on v1 (non-current) must detect the collision, not just compare against master
138+ try {
139+ await putData ( key , { versionId : v1VersionId } ) ;
140+ assert . fail ( 'expected VersionIdCollisionException' ) ;
141+ } catch ( err ) {
142+ assert . ok (
143+ err instanceof VersionIdCollisionException ,
144+ `expected VersionIdCollisionException, got ${ err . constructor . name } ` ,
145+ ) ;
146+ assert . strictEqual ( err . microVersionId , '' , 'microVersionId should be empty for original write state' ) ;
147+ }
148+ } ) ;
109149} ) ;
110150
111151describe ( 'putData : null-version objects (ExternalNullVersionId)' , ( ) => {
112- // Null-version objects created before versioning was enabled use Arsenal constant ExternalNullVersionId = 'null'
113- // getEncodedVersionId() returns 'null' as-is (no base62 encoding), and objMd.versionId is
114- // undefined in metadata : collision detection is not possible, so putData must write normally.
115- it ( 'should write normally when VersionId is "null" (ExternalNullVersionId)' , async ( ) => {
152+ it ( 'should write normally when VersionId is "null" and destination has no null version' , async ( ) => {
153+ // Versioned bucket: existing object has a real versionId, not a null version.
154+ // Fetching with ExternalNullVersionId returns no objMd => no collision => write normally.
155+ const key = 'putdata-null-version-no-collision' ;
156+ await s3 . send (
157+ new PutObjectCommand ( {
158+ Bucket : TEST_BUCKET ,
159+ Key : key ,
160+ Body : Buffer . from ( OBJECT_BODY ) ,
161+ ContentType : 'text/plain' ,
162+ } ) ,
163+ ) ;
164+
116165 const output = await backbeatClient . send (
117166 new PutDataCommand ( {
118167 Bucket : TEST_BUCKET ,
119- Key : 'putdata-null-version' ,
168+ Key : key ,
120169 ContentMD5 : OBJECT_MD5_HEX ,
121170 CanonicalID : CANONICAL_ID ,
122171 VersioningRequired : true ,
@@ -126,6 +175,39 @@ describe('putData : null-version objects (ExternalNullVersionId)', () => {
126175 ) ;
127176 assert . ok ( output . Location , 'putData with null-version versionId should write normally' ) ;
128177 } ) ;
178+
179+ it ( 'should throw VersionIdCollisionException when a null version already exists at destination' , async ( ) => {
180+ // Unversioned bucket: objects have no versionId in metadata (they are null versions).
181+ // putData with ExternalNullVersionId must detect the collision.
182+ const key = 'putdata-null-version-collision' ;
183+ await s3 . send (
184+ new PutObjectCommand ( {
185+ Bucket : TEST_BUCKET_UNVERSIONED ,
186+ Key : key ,
187+ Body : Buffer . from ( OBJECT_BODY ) ,
188+ ContentType : 'text/plain' ,
189+ } ) ,
190+ ) ;
191+
192+ try {
193+ await backbeatClient . send (
194+ new PutDataCommand ( {
195+ Bucket : TEST_BUCKET_UNVERSIONED ,
196+ Key : key ,
197+ ContentMD5 : OBJECT_MD5_HEX ,
198+ CanonicalID : CANONICAL_ID ,
199+ VersionId : ExternalNullVersionId ,
200+ Body : Buffer . from ( OBJECT_BODY ) ,
201+ } ) ,
202+ ) ;
203+ assert . fail ( 'expected VersionIdCollisionException' ) ;
204+ } catch ( err ) {
205+ assert . ok (
206+ err instanceof VersionIdCollisionException ,
207+ `expected VersionIdCollisionException, got ${ err . constructor . name } ` ,
208+ ) ;
209+ }
210+ } ) ;
129211} ) ;
130212
131213describe ( 'putData : baseline (no cascade headers)' , ( ) => {
0 commit comments