mattaddy / SObjectFabricator

An SObject fabrication API to reduce database interactions and dependencies on triggers in Apex unit tests.
Other
89 stars 14 forks source link

Mocking ContentDocumentLink is erroring out #16

Closed PseudoDarwinist closed 3 years ago

PseudoDarwinist commented 3 years ago

Hi, I am not sure if this is the right forum to ask but I am trying to mock ContentDocumentLink object as shown below but it is giving me error : Error Message System.JSONException: Cannot deserialize instance of <unknown> from VALUE_STRING value YWJj or request may be missing a required field Can you please let me know if there is a better way to mock CDLs.

// given mock CDLs

ContentDocumentLink[] mockCDLs = new List<ContentDocumentLink> {
  (ContentDocumentLink) new sfab_FabricatedSObject( ContentDocumentLink.class )
   .set(ContentDocumentLink.Id,fflib_IDGenerator.generate(ContentDocumentLink.SObjectType)
   .set(ContentDocumentLink.LinkedEntityId,fflib_IDGenerator.generate(Opportunity.SObjectType)
   .set('ContentDocument.LatestPublishedVersion.VersionData',Blob.valueOf('abc'))
   .toSobject(),
bobalicious commented 3 years ago

Yep, this is the right place to raise bugs with the SObject Fabricator.

TL/DR - Right now, it looks like mocking ContentVersion is not possible due to the inability to deserialise them from their JSON representation.

That is - the example failure can be simplified to:

ContentDocumentLink mockCdl = (ContentDocumentLink)new sfab_FabricatedSObject( ContentDocumentLink.class )
   .set( 'ContentDocument.LatestPublishedVersion.VersionData', Blob.valueOf( 'abc' ) )
   .toSobject();

Or even further to:

ContentVersion mockVersion = (ContentVersion)new sfab_FabricatedSObject( ContentVersion.class )
    .set( ContentVersion.VersionData, Blob.valueOf( 'abc' ) )
    .toSobject();

Which throws:

System.JSONException: Cannot deserialize instance of base64 from VALUE_STRING value YWJj or request may be missing a required field at [line:1, column:2]

Removing the Blob from the equation gives us:

ContentVersion mockVersion = (ContentVersion)new sfab_FabricatedSObject( ContentVersion.class )
    .set( ContentVersion.VersionData, 'abc' )
    .toSobject();

Which throws:

System.JSONException: Cannot deserialize instance of base64 from VALUE_STRING value abc or request may be missing a required field at [line:1, column:2]

Since the library uses a JSON deserialise in order to attempt to build the SObjects, looking at the behaviour of the real ContentVersion to see if it can be deserialised and reserialised, we can:

ContentVersion realVersion = new ContentVersion( VersionData = Blob.valueOf( 'abc' ) );
System.debug( (SObject)JSON.deserialize( JSON.serialize( realVersion ), ContentVersion.class ) );

Which throws:

System.JSONException: Cannot deserialize instance of base64 from START_OBJECT value { or request may be missing a required field at [line:1, column:40]

Whilst this isn't exactly the same error, it does look like there's something wrong with the JSON technique when applied to ContentVersion.

For information purposes - outputting the serialised version of the object we get:

{"attributes":{"type":"ContentVersion"},"VersionData":{"asByteArray":"YWJj","inputStream":{},"length":3,"maxToKeep":4}}

Without having a working example of ContentVersion, it's hard to see a simple way forward. Will continue to think and investigate to see if there is a workaround or solution, but it looks like we can't mock ContentVersion at the moment.

Potentially, changing the API number of the fabricator class may resolve it (if previous versions did work) - though don't have the time to look into that right now.

Aside: looking at the behaviour of Blobs, they look like they can be serialised / deserialised without error.

I.E. the following:

System.debug( JSON.deserialize( JSON.serialize( Blob.valueOf( 'abc' ) ), Blob.class ) );

Outputs:

DEBUG|Blob[3]

bobalicious commented 3 years ago

Can't currently think of a way round this - it appears that there is a property missing from the JSON that the deserialise needs, but cannot create a working example in order to find what that property is.

cropredyHelix commented 3 years ago

@bobalicious - this is related to Stackexchange Q&A (my answer).

and BTW, it fails on the sfab version prior to your PR

fails for Attachment as well so not specifically a ContentVersion issue but a Blob issue as below gets same error

Attachment atch = (Attachment) new sfab_FabricatedSObject(Attachment.class)
    .setField(Attachment.Body,Blob.valueOf('abc'))
    .toSObject();

see also @sfdcfox answer: https://salesforce.stackexchange.com/a/130911/2602

see also PR for andy fawcett DataLoader repo that makes the blob value null before deserialize/serialize then adds it back. This suggests a sfab workaround that recognizes if the field value is instance of Blob that you stash the blob value elsewhere, null it out before toSobject() and then use sobject.put to add back the stashed blob. This gets a bit tricky when the blob is in a parent or child sobject

bobalicious commented 3 years ago

@cropredyHelix - Excellent idea - thanks!

I'll investigate and see what we can do in terms of storing a map of the Blob fields and applying them after deserialisation. It's probably going to be clunky under the hood, but hopefully the way the code is structured under the hood means it shouldn't be too big a deal to recognise and assign the blobs to the maps when the object is first being built - assigning the right blobs to the right SObjects might be trickier, though there's an object per SObject under the hood, so it's just a case of tying those together.

Will have an experiment and a think.

bobalicious commented 3 years ago

@PseudoDarwinist

A current work-around is to build the Content Document Links without the data set (but with another field set), build the SObjects, and then apply the version data to the resulting SObject.

For example:

ContentDocumentLink cdl =
  (ContentDocumentLink) new sfab_FabricatedSObject( ContentDocumentLink.class )
   .set( ContentDocumentLink.Id, '123' )
   .set( ContentDocumentLink.LinkedEntityId, '456' )
   .set( 'ContentDocument.LatestPublishedVersion.LastModifiedDate', System.now() ) // this ensures that the child object exists in the structure
   .toSobject();

cdl.ContentDocument.LatestPublishedVersion.VersionData = Blob.valueOf( 'abc' ); // directly set the Blob value on the child object after generation

// DEBUG|ContentDocumentLink:{LinkedEntityId=456, Id=123}
System.debug( cdl );

// DEBUG|ContentDocument:{}
System.debug( cdl.ContentDocument );

// DEBUG|ContentVersion:{LastModifiedDate=2021-04-20 07:14:30, VersionData=Blob[3]}
System.debug( cdl.ContentDocument.LatestPublishedVersion );

I know it's not the most elegant of solutions, but hopefully it will unblock you whilst we work on a fix to the library.

bobalicious commented 3 years ago

Proof of concept of a fix has been implemented on a fork: https://github.com/bobalicious/SObjectFabricator/tree/feature/%2316-allow-blob-fields-to-be-populated-on-objects

Note, this is only at PoC stage at the moment and needs more testing - feel free to test, but use very much at your own risk.

In particular, investigation needed for:

bobalicious commented 3 years ago

This issue has now been resolved on master, and the following code now works as expected (note you can now set Base64 fields using either a Blob or a String):

ContentDocumentLink cdl =
  (ContentDocumentLink) new sfab_FabricatedSObject( ContentDocumentLink.class )
   .set( ContentDocumentLink.Id, '123' )
   .set( ContentDocumentLink.LinkedEntityId, '456' )
   .set( 'ContentDocument.LatestPublishedVersion.VersionData', 'abc' ) 
   .toSobject();

// DEBUG|ContentDocumentLink:{LinkedEntityId=456, Id=123}
System.debug( cdl );

// DEBUG|ContentDocument:{}
System.debug( cdl.ContentDocument );

// DEBUG|ContentVersion:{VersionData=Blob[3]}
System.debug( cdl.ContentDocument.LatestPublishedVersion );