diff --git a/Modules/System Tests/Azure Blob Services API/src/ABSBlobClientTest.Codeunit.al b/Modules/System Tests/Azure Blob Services API/src/ABSBlobClientTest.Codeunit.al index e38dc58202..6343d1d0c8 100644 --- a/Modules/System Tests/Azure Blob Services API/src/ABSBlobClientTest.Codeunit.al +++ b/Modules/System Tests/Azure Blob Services API/src/ABSBlobClientTest.Codeunit.al @@ -18,7 +18,7 @@ codeunit 132920 "ABS Blob Client Test" Response: Codeunit "ABS Operation Response"; ContainerName, BlobName, BlobContent, NewBlobContent : Text; begin - // [Scenarion] Given a storage account and a container, PutBlobBlockBlob operation succeeds and GetBlobAsText returns the content + // [Scenario] Given a storage account and a container, PutBlobBlockBlob operation succeeds and GetBlobAsText returns the content SharedKeyAuthorization := StorageServiceAuthorization.CreateSharedKey(AzuriteTestLibrary.GetAccessKey()); @@ -53,7 +53,7 @@ codeunit 132920 "ABS Blob Client Test" Response: Codeunit "ABS Operation Response"; ContainerName, FirstBlobName, SecondBlobName, BlobContent : Text; begin - // [Scenarion] Given a storage account and a container with BLOBs, ListBlobs operation succeeds. + // [Scenario] Given a storage account and a container with BLOBs, ListBlobs operation succeeds. SharedKeyAuthorization := StorageServiceAuthorization.CreateSharedKey(AzuriteTestLibrary.GetAccessKey()); @@ -101,7 +101,7 @@ codeunit 132920 "ABS Blob Client Test" BlobList: Dictionary of [Text, XmlNode]; Blobs: List of [Text]; begin - // [Scenarion] Given a storage account and a container with BLOBs, ListBlobs operation succeeds. + // [Scenario] Given a storage account and a container with BLOBs, ListBlobs operation succeeds. SharedKeyAuthorization := StorageServiceAuthorization.CreateSharedKey(AzuriteTestLibrary.GetAccessKey()); @@ -200,7 +200,7 @@ codeunit 132920 "ABS Blob Client Test" ABSContainerContent: Record "ABS Container Content"; ContainerName: Text; begin - // [Scenarion] When listing blobs, the levels, parent directories etc. are set correctly + // [Scenario] When listing blobs, the levels, parent directories etc. are set correctly SharedKeyAuthorization := StorageServiceAuthorization.CreateSharedKey(AzuriteTestLibrary.GetAccessKey()); @@ -538,7 +538,7 @@ codeunit 132920 "ABS Blob Client Test" LeaseId: Guid; ProposedLeaseId: Guid; begin - // [Scenarion] Given a storage account and a container, PutBlobBlockBlob operation succeeds and subsequent lease-operations + // [Scenario] Given a storage account and a container, PutBlobBlockBlob operation succeeds and subsequent lease-operations // (1) create a lease, (2) renew a lease, [(3) change a lease], (4) break a lease and (5) release the lease SharedKeyAuthorization := StorageServiceAuthorization.CreateSharedKey(AzuriteTestLibrary.GetAccessKey()); @@ -665,6 +665,68 @@ codeunit 132920 "ABS Blob Client Test" ABSContainerClient.DeleteContainer(ContainerName); end; + [Test] + procedure ParseResponseWithHierarchicalBlobName() + var + ABSContainerContent: Record "ABS Container Content"; + ABSHelperLibrary: Codeunit "ABS Helper Library"; + NodeList: XmlNodeList; + NextMarker: Text; + begin + // [SCENARIO] Parse the BLOB storage API response listing BLOBs in a container without the hierarchical namespace + + // [GIVEN] Prepare an XML response from the BLOB Storage API, listing all BLOBs in a container. + // [GIVEN] Container does not have hierarchical namespace and contains a single BLOB with the name like "folder/blob" + NodeList := ABSHelperLibrary.CreateBlobNodeListFromResponse(ABSTestLibrary.GetServiceResponseBlobWithHierarchicalName(), NextMarker); + + // [WHEN] Invoke BlobNodeListToTempRecord + ABSHelperLibrary.BlobNodeListToTempRecord(NodeList, ABSContainerContent); + + // [THEN] "ABS Container Content" contains one record "folder" and one record "blob" + Assert.RecordCount(ABSContainerContent, 2); + + ABSContainerContent.SetRange(Name, ABSTestLibrary.GetSampleResponseRootDirName()); + Assert.RecordCount(ABSContainerContent, 1); + + ABSContainerContent.SetRange(Name, ABSTestLibrary.GetSampleResponseFileName()); + Assert.RecordCount(ABSContainerContent, 1); + end; + + [Test] + procedure ParseResponseFromStorageHierarachicalNamespace() + var + ABSContainerContent: Record "ABS Container Content"; + ABSHelperLibrary: Codeunit "ABS Helper Library"; + NodeList: XmlNodeList; + NextMarker: Text; + begin + // [SCENARIO] Parse the BLOB storage API response listing BLOBs in a container with the hierarchical namespace enabled + + // [GIVEN] Prepare an XML response from the BLOB Storage API, listing all BLOBs in a container. + // [GIVEN] Container has the hierarchical namespace enabled, and contains a root folder named "rootdir". + // [GIVEN] There is a subdirectory "subdir" in the root, and one blob named "blob" in the subdirectory. + NodeList := ABSHelperLibrary.CreateBlobNodeListFromResponse(ABSTestLibrary.GetServiceResponseHierarchicalNamespace(), NextMarker); + + // [WHEN] Invoke BlobNodeListToTempRecord + ABSHelperLibrary.BlobNodeListToTempRecord(NodeList, ABSContainerContent); + + // [THEN] "ABS Container Content" contains two records with resource type "folder" and one record with resource type = "blob" + Assert.RecordCount(ABSContainerContent, 3); + + VerifyContainerContentType(ABSContainerContent, CopyStr(ABSTestLibrary.GetSampleResponseRootDirName(), 1, MaxStrLen(ABSContainerContent.Name)), Enum::"ABS Blob Resource Type"::Directory); + VerifyContainerContentType(ABSContainerContent, CopyStr(ABSTestLibrary.GetSampleResponseSubdirName(), 1, MaxStrLen(ABSContainerContent.Name)), Enum::"ABS Blob Resource Type"::Directory); + VerifyContainerContentType(ABSContainerContent, CopyStr(ABSTestLibrary.GetSampleResponseFileName(), 1, MaxStrLen(ABSContainerContent.Name)), Enum::"ABS Blob Resource Type"::File); + end; + + local procedure VerifyContainerContentType(var ABSContainerContent: Record "ABS Container Content"; BlobName: Text[2048]; ExpectedResourceType: Enum "ABS Blob Resource Type") + var + IncorrectBlobPropertyErr: Label 'BLOB property is assigned incorrectly'; + begin + ABSContainerContent.SetRange(Name, BlobName); + ABSContainerContent.FindFirst(); + Assert.AreEqual(ExpectedResourceType, ABSContainerContent."Resource Type", IncorrectBlobPropertyErr); + end; + local procedure GetBlobTagsFromABSContainerBlobList(BlobName: Text; BlobList: Dictionary of [Text, XmlNode]): Dictionary of [Text, Text] var BlobNode: XmlNode; @@ -715,4 +777,4 @@ codeunit 132920 "ABS Blob Client Test" ABSTestLibrary: Codeunit "ABS Test Library"; AzuriteTestLibrary: Codeunit "Azurite Test Library"; SharedKeyAuthorization: Interface "Storage Service Authorization"; -} \ No newline at end of file +} diff --git a/Modules/System Tests/Azure Blob Services API/src/ABSTestLibrary.Codeunit.al b/Modules/System Tests/Azure Blob Services API/src/ABSTestLibrary.Codeunit.al index d55eaf8fc0..48a903285a 100644 --- a/Modules/System Tests/Azure Blob Services API/src/ABSTestLibrary.Codeunit.al +++ b/Modules/System Tests/Azure Blob Services API/src/ABSTestLibrary.Codeunit.al @@ -156,6 +156,137 @@ codeunit 132921 "ABS Test Library" exit(Document); end; + procedure GetServiceResponseBlobWithHierarchicalName(): Text; + var + Builder: TextBuilder; + begin + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append('rootdir/filename.txt'); + Builder.Append('https://myaccount.blob.core.windows.net/mycontainer/rootdir/filename.txt'); + Builder.Append(''); + Builder.Append('Sat, 23 Sep 2023 21:32:55 GMT'); + Builder.Append('0x8DBBC7CA6253661'); + Builder.Append('1'); + Builder.Append('text/plain'); + Builder.Append(''); + Builder.Append(''); + Builder.Append('dpT0pmMW5TyM3Z2ZVL1hHQ=='); + Builder.Append(''); + Builder.Append('BlockBlob'); + Builder.Append('unlocked'); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + + exit(Builder.ToText()); + end; + + procedure GetServiceResponseHierarchicalNamespace(): Text; + var + Builder: TextBuilder; + begin + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append('rootdir'); + Builder.Append(''); + Builder.Append('Sat, 23 Sep 2023 21:04:18 GMT'); + Builder.Append('Sat, 23 Sep 2023 21:04:18 GMT'); + Builder.Append('0x8DBBC78A6E95AF7'); + Builder.Append('directory'); + Builder.Append('0'); + Builder.Append('application/octet-stream'); + Builder.Append(''); + Builder.Append(''); + Builder.Append('AAAAAAAAAAA='); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append('BlockBlob'); + Builder.Append('Hot'); + Builder.Append('true'); + Builder.Append('unlocked'); + Builder.Append('available'); + Builder.Append('true'); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append('rootdir/subdirectory'); + Builder.Append(''); + Builder.Append('Tue, 26 Sep 2023 21:47:48 GMT'); + Builder.Append('Tue, 26 Sep 2023 21:47:48 GMT'); + Builder.Append('0x8DBBEDA39F9C41D'); + Builder.Append('directory'); + Builder.Append('0'); + Builder.Append('application/octet-stream'); + Builder.Append(''); + Builder.Append(''); + Builder.Append('AAAAAAAAAAA='); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append('BlockBlob'); + Builder.Append('Hot'); + Builder.Append('true'); + Builder.Append('unlocked'); + Builder.Append('available'); + Builder.Append('true'); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append('rootdir/subdirectory/filename.txt'); + Builder.Append(''); + Builder.Append('Wed, 27 Sep 2023 20:24:18 GMT'); + Builder.Append('Wed, 27 Sep 2023 20:24:18 GMT'); + Builder.Append('0x8DBBF97BA4A9839'); + Builder.Append('file'); + Builder.Append('1'); + Builder.Append('text/plain'); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append('dpT0pmMW5TyM3Z2ZVL1hHQ=='); + Builder.Append(''); + Builder.Append(''); + Builder.Append('BlockBlob'); + Builder.Append('Hot'); + Builder.Append('true'); + Builder.Append('unlocked'); + Builder.Append('available'); + Builder.Append('true'); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + Builder.Append(''); + + exit(Builder.ToText()); + end; + + procedure GetSampleResponseRootDirName(): Text + begin + exit('rootdir'); + end; + + procedure GetSampleResponseSubdirName(): Text + begin + exit('subdirectory'); + end; + + procedure GetSampleResponseFileName(): Text + begin + exit('filename.txt'); + end; + local procedure GetNewLineCharacter(): Text var LF: Char; diff --git a/Modules/System/Azure Blob Services API/src/Helper/ABSContainerContentHelper.Codeunit.al b/Modules/System/Azure Blob Services API/src/Helper/ABSContainerContentHelper.Codeunit.al index 4ed0928477..0b85302a8d 100644 --- a/Modules/System/Azure Blob Services API/src/Helper/ABSContainerContentHelper.Codeunit.al +++ b/Modules/System/Azure Blob Services API/src/Helper/ABSContainerContentHelper.Codeunit.al @@ -25,15 +25,19 @@ codeunit 9054 "ABS Container Content Helper" Node.SelectSingleNode('.//Properties', PropertiesNode); ChildNodes := PropertiesNode.AsXmlElement().GetChildNodes(); - AddNewEntry(ABSContainerContent, NameFromXml, OuterXml, ChildNodes, EntryNo); + AddNewEntry(ABSContainerContent, NameFromXml, OuterXml, ChildNodes, EntryNo, GetBlobResourceType(Node)); end; [NonDebuggable] - procedure AddNewEntry(var ABSContainerContent: Record "ABS Container Content"; NameFromXml: Text; OuterXml: Text; ChildNodes: XmlNodeList; var EntryNo: Integer) + procedure AddNewEntry( + var ABSContainerContent: Record "ABS Container Content"; NameFromXml: Text; OuterXml: Text; ChildNodes: XmlNodeList; var EntryNo: Integer; ResourceType: Enum "ABS Blob Resource Type") var OutStream: OutStream; begin - AddParentEntries(NameFromXml, ABSContainerContent, EntryNo); + if ResourceType = ResourceType::Directory then + ParentEntryFullNameList.Add(NameFromXml) + else + AddParentEntries(NameFromXml, ABSContainerContent, EntryNo); ABSContainerContent.Init(); ABSContainerContent.Level := GetLevel(NameFromXml); @@ -199,6 +203,22 @@ codeunit 9054 "ABS Container Content Helper" Node := Document.AsXmlNode(); end; + local procedure GetBlobResourceType(BlobXmlNode: XmlNode): Enum "ABS Blob Resource Type" + var + ResourceTypeNode: XmlNode; + begin + if not BlobXmlNode.SelectSingleNode('.//ResourceType', ResourceTypeNode) then + exit(Enum::"ABS Blob Resource Type"::File); + + case ResourceTypeNode.AsXmlElement().InnerText().ToLower() of + Format(Enum::"ABS Blob Resource Type"::File).ToLower(): + exit(Enum::"ABS Blob Resource Type"::File); + + Format(Enum::"ABS Blob Resource Type"::Directory).ToLower(): + exit(Enum::"ABS Blob Resource Type"::Directory) + end; + end; + var ParentEntryFullNameList: List of [Text]; } \ No newline at end of file