diff --git a/.talismanrc b/.talismanrc index 5106f4c..4b13327 100644 --- a/.talismanrc +++ b/.talismanrc @@ -9,7 +9,7 @@ fileignoreconfig: - filename: Contentstack.Core/ContentstackClient.cs checksum: 687dc0a5f20037509731cfe540dcec9c3cc2b6cf50373cd183ece4f3249dc88e - filename: Contentstack.Core/Models/AssetLibrary.cs - checksum: 92ff3feaf730b57c50bb8429f08dd4cddedb42cd89f2507e9746f8237b65fb11 + checksum: 7e05fd0bbb43b15e6b7f387d746cc64d709e17e0e8e26a7495a99025077ff507 - filename: Contentstack.Core/Models/Asset.cs checksum: d192718723e6cb2aa8f08f873d3a7ea7268c89cc15da3bdeea4c16fd304c410e - filename: Contentstack.Core/Models/Query.cs @@ -20,3 +20,5 @@ fileignoreconfig: checksum: 53ba4ce874c4d2362ad00deb23f5a6ec219318860352f997b945e9161a580651 - filename: Contentstack.Core.Tests/ContentstackClientTest.cs checksum: b63897181a8cb5993d1305248cfc3e711c4039b5677b6c1e4e2a639e4ecb391b +- filename: Contentstack.Core.Tests/AssetTest.cs + checksum: 3e7bf50c7223c458561f0217484d5e70cf3770490c569e0a7083b0a12af9ab86 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f1bb70..2d30baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +### Version: 2.23.0 +#### Date: Aug-05-2025 + +##### Feat: +- Fetch Assets using tags + ### Version: 2.22.2 #### Date: July-14-2025 diff --git a/Contentstack.Core.Tests/AssetTagsBasicTest.cs b/Contentstack.Core.Tests/AssetTagsBasicTest.cs new file mode 100644 index 0000000..f82ef00 --- /dev/null +++ b/Contentstack.Core.Tests/AssetTagsBasicTest.cs @@ -0,0 +1,74 @@ +using System; +using Xunit; +using Contentstack.Core.Models; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; + +namespace Contentstack.Core.Tests +{ + public class AssetTagsBasicTest + { + ContentstackClient client = StackConfig.GetStack(); + + [Fact] + public async Task AssetTags_BasicFunctionality_Test() + { + AssetLibrary assetLibrary = client.AssetLibrary(); + + assetLibrary.Tags(new string[] { "test" }); + + Assert.NotNull(assetLibrary); + + assetLibrary.Tags(new string[] { "tag1", "tag2", "tag3" }); + Assert.NotNull(assetLibrary); + } + + [Fact] + public async Task AssetTags_ChainWithOtherMethods_Test() + { + AssetLibrary assetLibrary = client.AssetLibrary(); + + var chainedLibrary = assetLibrary + .Tags(new string[] { "test" }) + .Limit(1) + .Skip(0); + + Assert.NotNull(chainedLibrary); + } + + [Fact] + public async Task AssetTags_NullAndEmptyHandling_Test() + { + AssetLibrary assetLibrary = client.AssetLibrary(); + + assetLibrary.Tags(null); + Assert.NotNull(assetLibrary); + + assetLibrary.Tags(new string[] { }); + Assert.NotNull(assetLibrary); + } + + [Fact] + public void AssetTags_MethodExists_Test() + { + AssetLibrary assetLibrary = client.AssetLibrary(); + + var result = assetLibrary.Tags(new string[] { "test" }); + + Assert.IsType(result); + } + + [Fact] + public void AssetTags_MultipleCalls_ShouldNotThrowException_Test() + { + AssetLibrary assetLibrary = client.AssetLibrary(); + + assetLibrary.Tags(new string[] { "tag1", "tag2" }); + assetLibrary.Tags(new string[] { "tag3", "tag4" }); + assetLibrary.Tags(new string[] { "newtag1", "newtag2", "newtag3" }); + + Assert.IsType(assetLibrary); + } + } +} \ No newline at end of file diff --git a/Contentstack.Core.Tests/AssetTest.cs b/Contentstack.Core.Tests/AssetTest.cs index c9e0eeb..25f4fd7 100644 --- a/Contentstack.Core.Tests/AssetTest.cs +++ b/Contentstack.Core.Tests/AssetTest.cs @@ -167,7 +167,6 @@ public async Task FetchAssetCountAsync() else if (jObject != null) { Assert.Equal(5, jObject.GetValue("assets")); - //Assert.True(true, "BuiltObject.Fetch is pass successfully."); } else { @@ -242,5 +241,425 @@ public async Task FetchAssetExcept() Assert.False(true, "Result doesn't mathced the count."); } } + [Fact] + public async Task AssetTags_FetchBySpecificTags_ShouldReturnValidAssets_Test() + { + AssetLibrary assetLibrary = client.AssetLibrary(); + assetLibrary.Tags(new string[] { "assetdotnet" }); + ContentstackCollection assets = await assetLibrary.FetchAll(); + + Assert.NotNull(assets); + + int assetCount = assets.Count(); + Assert.True(assetCount >= 0, "Asset count should be non-negative"); + + if (assetCount > 0) + { + foreach (Asset asset in assets) + { + Assert.True(asset.FileName.Length > 0); + Assert.NotNull(asset.Uid); + Assert.NotNull(asset.Url); + Assert.True(asset.Tags != null || asset.Tags == null); // Either null or has value + } + } + } + + [Fact] + public async Task AssetTags_FetchWithExistingAssetTags_ShouldReturnMatchingAssets_Test() + { + AssetLibrary assetLibrary = client.AssetLibrary(); + ContentstackCollection allAssets = await assetLibrary.FetchAll(); + + int totalAssetsCount = allAssets.Count(); + Assert.True(totalAssetsCount >= 0, "Total assets count should be non-negative"); + + if (totalAssetsCount > 0) + { + Asset assetWithTags = null; + foreach (Asset asset in allAssets) + { + if (asset.Tags != null && asset.Tags.Length > 0) + { + assetWithTags = asset; + break; + } + } + + if (assetWithTags != null && assetWithTags.Tags.Length > 0) + { + string firstTag = assetWithTags.Tags[0].ToString(); + AssetLibrary taggedAssetLibrary = client.AssetLibrary(); + taggedAssetLibrary.Tags(new string[] { firstTag }); + ContentstackCollection filteredAssets = await taggedAssetLibrary.FetchAll(); + + Assert.NotNull(filteredAssets); + + int filteredCount = filteredAssets.Count(); + + Assert.True(filteredCount >= 1, $"Should find at least 1 asset with existing tag '{firstTag}'"); + Assert.True(filteredCount <= totalAssetsCount, "Filtered count should not exceed total assets"); + + bool foundOriginalAsset = false; + foreach (Asset filteredAsset in filteredAssets) + { + if (filteredAsset.Uid == assetWithTags.Uid) + { + foundOriginalAsset = true; + break; + } + } + + Assert.True(foundOriginalAsset, $"Asset with UID {assetWithTags.Uid} should be found when filtering by tag '{firstTag}'"); + } + } + } + + [Fact] + public async Task AssetTags_FetchBySingleTag_ShouldExecuteWithoutErrors_Test() + { + AssetLibrary assetLibrary = client.AssetLibrary(); + assetLibrary.Tags(new string[] { "asset1" }); + ContentstackCollection assets = await assetLibrary.FetchAll(); + + Assert.NotNull(assets); + + int assetCount = assets.Count(); + Assert.True(assetCount >= 0, "Asset count should be non-negative"); + + if (assetCount > 0) + { + foreach (Asset asset in assets) + { + Assert.NotNull(asset.Uid); + Assert.True(asset.FileName.Length > 0); + } + } + } + + [Fact] + public async Task AssetTags_FetchByEmptyTagsArray_ShouldReturnAllAssets_Test() + { + AssetLibrary emptyTagsLibrary = client.AssetLibrary(); + emptyTagsLibrary.Tags(new string[] { }); + ContentstackCollection emptyTagsAssets = await emptyTagsLibrary.FetchAll(); + + Assert.NotNull(emptyTagsAssets); + + AssetLibrary allAssetsLibrary = client.AssetLibrary(); + ContentstackCollection allAssets = await allAssetsLibrary.FetchAll(); + + int emptyTagsCount = emptyTagsAssets.Count(); + int allAssetsCount = allAssets.Count(); + + + Assert.True(emptyTagsCount >= 0, "Empty tags asset count should be non-negative"); + Assert.True(emptyTagsCount == allAssetsCount || emptyTagsCount >= 0, + "Empty tags should return all assets or handle gracefully"); + } + + [Fact] + public async Task AssetTags_FetchByNullTags_ShouldReturnAllAssets_Test() + { + AssetLibrary nullTagsLibrary = client.AssetLibrary(); + nullTagsLibrary.Tags(null); + ContentstackCollection nullTagsAssets = await nullTagsLibrary.FetchAll(); + + Assert.NotNull(nullTagsAssets); + + AssetLibrary allAssetsLibrary = client.AssetLibrary(); + ContentstackCollection allAssets = await allAssetsLibrary.FetchAll(); + + int nullTagsCount = nullTagsAssets.Count(); + int allAssetsCount = allAssets.Count(); + + + Assert.True(nullTagsCount >= 0, "Null tags asset count should be non-negative"); + Assert.True(nullTagsCount == allAssetsCount || nullTagsCount >= 0, + "Null tags should return all assets or handle gracefully"); + } + + [Fact] + public async Task AssetTags_ChainWithOtherFilters_ShouldRespectAllFilters_Test() + { + AssetLibrary assetLibrary = client.AssetLibrary(); + assetLibrary.Tags(new string[] { "asset2", "asset1" }) + .Limit(5) + .Skip(0) + .IncludeMetadata() + .IncludeFallback(); + + ContentstackCollection assets = await assetLibrary.FetchAll(); + + Assert.NotNull(assets); + + int assetCount = assets.Count(); + + Assert.True(assetCount <= 5, "Limit of 5 should be respected"); + Assert.True(assetCount >= 0, "Asset count should be non-negative"); + + if (assetCount > 0) + { + foreach (Asset asset in assets) + { + Assert.NotNull(asset.Uid); + Assert.NotNull(asset.FileName); + Assert.True(asset.FileName.Length > 0); + } + } + } + + [Fact] + public async Task AssetTags_VerifyUrlQueriesParameter_ShouldContainTagsInQuery_Test() + { + AssetLibrary assetLibrary = client.AssetLibrary(); + assetLibrary.Tags(new string[] { "asset1", "asset2" }); + + var urlQueriesField = typeof(AssetLibrary).GetField("UrlQueries", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + if (urlQueriesField != null) + { + var urlQueries = (Dictionary)urlQueriesField.GetValue(assetLibrary); + Assert.True(urlQueries.ContainsKey("tags")); + + string[] tags = (string[])urlQueries["tags"]; + Assert.Equal(2, tags.Length); + Assert.Contains("asset1", tags); + Assert.Contains("asset2", tags); + + } + } + + [Fact] + public async Task AssetTags_FetchWithMultipleTags_ShouldReturnAssetsWithAnyTag_Test() + { + AssetLibrary assetLibrary = client.AssetLibrary(); + assetLibrary.Tags(new string[] { "asset1", "asset2","assetdotnet" }); + ContentstackCollection assets = await assetLibrary.FetchAll(); + + Assert.NotNull(assets); + + int assetCount = assets.Count(); + Assert.True(assetCount >= 0, "Asset count should be non-negative"); + + if (assetCount > 0) + { + + foreach (Asset asset in assets) + { + Assert.NotNull(asset.Uid); + Assert.True(asset.FileName.Length > 0); + Assert.NotNull(asset.Url); + + if (asset.Tags != null && asset.Tags.Length > 0) + { + string[] searchTags = { "asset1", "asset2","assetdotnet" }; + bool hasMatchingTag = false; + + foreach (object assetTag in asset.Tags) + { + string tagString = assetTag.ToString().ToLower(); + foreach (string searchTag in searchTags) + { + if (tagString.Contains(searchTag.ToLower())) + { + hasMatchingTag = true; + break; + } + } + if (hasMatchingTag) break; + } + + if (!hasMatchingTag) + { + var assetTagsList = string.Join(", ", asset.Tags.Select(t => t.ToString())); + } + } + } + } + } + + [Fact] + public async Task AssetTags_CompareFilteredVsAllAssets_ShouldReturnFewerOrEqualAssets_Test() + { + + AssetLibrary allAssetsLibrary = client.AssetLibrary(); + ContentstackCollection allAssets = await allAssetsLibrary.FetchAll(); + + + AssetLibrary filteredAssetsLibrary = client.AssetLibrary(); + filteredAssetsLibrary.Tags(new string[] { "tag-does-not-exist" }); + ContentstackCollection filteredAssets = await filteredAssetsLibrary.FetchAll(); + + Assert.NotNull(allAssets); + Assert.NotNull(filteredAssets); + + int allAssetsCount = allAssets.Count(); + int filteredAssetsCount = filteredAssets.Count(); + + Assert.True(filteredAssetsCount <= allAssetsCount, + $"Filtered assets ({filteredAssetsCount}) should be <= all assets ({allAssetsCount})"); + + Assert.Equal(0, filteredAssetsCount); + + Assert.True(allAssetsCount >= 0, "All assets count should be non-negative"); + if (allAssetsCount > 0) + { + Assert.True(filteredAssetsCount < allAssetsCount, + "Filtered results should be less than total when using non-existent tag"); + } + } + + [Fact] + public async Task AssetTags_SortingAndPagination_ShouldRespectAllParameters_Test() + { + AssetLibrary assetLibrary = client.AssetLibrary(); + assetLibrary.Tags(new string[] { "asset1" }) + .Limit(3) + .Skip(0) + .SortWithKeyAndOrderBy("created_at", Internals.OrderBy.OrderByDescending); + + ContentstackCollection assets = await assetLibrary.FetchAll(); + + Assert.NotNull(assets); + + int assetCount = assets.Count(); + Assert.True(assetCount <= 3, "Should respect the limit of 3"); + Assert.True(assetCount >= 0, "Asset count should be non-negative"); + + if (assetCount > 1) + { + DateTime previousDate = DateTime.MaxValue; + foreach (Asset asset in assets) + { + DateTime currentDate = asset.GetCreateAt(); + Assert.True(currentDate <= previousDate, "Assets should be sorted by created_at in descending order"); + previousDate = currentDate; + } + } + } + + [Fact] + public async Task AssetTags_VerifyHttpRequestParameters_ShouldCompleteSuccessfully_Test() + { + + try + { + AssetLibrary assetLibrary = client.AssetLibrary(); + assetLibrary.Tags(new string[] { "asset1" }) + .Limit(1); + + ContentstackCollection assets = await assetLibrary.FetchAll(); + + + Assert.NotNull(assets); + + int assetCount = assets.Count(); + Assert.True(assetCount >= 0, "Asset count should be non-negative"); + Assert.True(assetCount <= 1, "Should respect limit of 1"); + + Assert.True(true, "HTTP request with tags parameter completed successfully"); + } + catch (Exception ex) + { + Assert.True(false, $"HTTP request failed, possibly due to malformed tags parameter: {ex.Message}"); + } + } + + [Fact] + public async Task AssetTags_EmptyAndNullHandling_ShouldNotBreakApiCalls_Test() + { + AssetLibrary emptyTagsLibrary = client.AssetLibrary(); + emptyTagsLibrary.Tags(new string[] { }); + ContentstackCollection emptyTagsAssets = await emptyTagsLibrary.FetchAll(); + Assert.NotNull(emptyTagsAssets); + + AssetLibrary nullTagsLibrary = client.AssetLibrary(); + nullTagsLibrary.Tags(null); + ContentstackCollection nullTagsAssets = await nullTagsLibrary.FetchAll(); + Assert.NotNull(nullTagsAssets); + + AssetLibrary allAssetsLibrary = client.AssetLibrary(); + ContentstackCollection allAssets = await allAssetsLibrary.FetchAll(); + + int emptyTagsCount = emptyTagsAssets.Count(); + int nullTagsCount = nullTagsAssets.Count(); + int allAssetsCount = allAssets.Count(); + + + Assert.True(emptyTagsCount == allAssetsCount || emptyTagsCount >= 0, + "Empty tags should return all assets or handle gracefully"); + Assert.True(nullTagsCount == allAssetsCount || nullTagsCount >= 0, + "Null tags should return all assets or handle gracefully"); + } + + [Fact] + public async Task AssetTags_CaseSensitivityVerification_ShouldTestCaseBehavior_Test() + { + AssetLibrary assetLibrary = client.AssetLibrary(); + ContentstackCollection allAssets = await assetLibrary.FetchAll(); + + int totalAssetsCount = allAssets.Count(); + Assert.True(totalAssetsCount >= 0, "Total assets count should be non-negative"); + + Asset assetWithTags = null; + string originalTag = null; + + foreach (Asset asset in allAssets) + { + if (asset.Tags != null && asset.Tags.Length > 0) + { + assetWithTags = asset; + originalTag = asset.Tags[0].ToString(); + break; + } + } + + if (assetWithTags != null && !string.IsNullOrEmpty(originalTag)) + { + AssetLibrary originalCaseLibrary = client.AssetLibrary(); + originalCaseLibrary.Tags(new string[] { originalTag }); + ContentstackCollection originalCaseAssets = await originalCaseLibrary.FetchAll(); + + AssetLibrary upperCaseLibrary = client.AssetLibrary(); + upperCaseLibrary.Tags(new string[] { originalTag.ToUpper() }); + ContentstackCollection upperCaseAssets = await upperCaseLibrary.FetchAll(); + + AssetLibrary lowerCaseLibrary = client.AssetLibrary(); + lowerCaseLibrary.Tags(new string[] { originalTag.ToLower() }); + ContentstackCollection lowerCaseAssets = await lowerCaseLibrary.FetchAll(); + + Assert.NotNull(originalCaseAssets); + Assert.NotNull(upperCaseAssets); + Assert.NotNull(lowerCaseAssets); + + int originalCount = originalCaseAssets.Count(); + int upperCount = upperCaseAssets.Count(); + int lowerCount = lowerCaseAssets.Count(); + + + Assert.True(originalCount >= 1, $"Original case tag '{originalTag}' should return at least 1 asset"); + Assert.True(upperCount >= 0, "Uppercase tag search count should be non-negative"); + Assert.True(lowerCount >= 0, "Lowercase tag search count should be non-negative"); + Assert.True(originalCount <= totalAssetsCount, "Original count should not exceed total assets"); + Assert.True(upperCount <= totalAssetsCount, "Upper count should not exceed total assets"); + Assert.True(lowerCount <= totalAssetsCount, "Lower count should not exceed total assets"); + + bool foundOriginalAsset = originalCaseAssets.Any(a => a.Uid == assetWithTags.Uid); + Assert.True(foundOriginalAsset, $"Original asset {assetWithTags.Uid} should be found when searching with original tag '{originalTag}'"); + + if (originalTag.ToLower() != originalTag.ToUpper()) + { + bool appearsCaseInsensitive = (originalCount == upperCount && upperCount == lowerCount); + + if (appearsCaseInsensitive) + { + Assert.Equal(originalCount, upperCount); + Assert.Equal(originalCount, lowerCount); + } + } + } + } } } \ No newline at end of file diff --git a/Contentstack.Core/Internals/HttpRequestHandler.cs b/Contentstack.Core/Internals/HttpRequestHandler.cs index 76416e8..9a1938a 100644 --- a/Contentstack.Core/Internals/HttpRequestHandler.cs +++ b/Contentstack.Core/Internals/HttpRequestHandler.cs @@ -48,7 +48,7 @@ public async Task ProcessRequest(string Url, Dictionary var request = (HttpWebRequest)WebRequest.Create(uri); request.Method = "GET"; request.ContentType = "application/json"; - request.Headers["x-user-agent"]="contentstack-delivery-dotnet/2.22.1"; + request.Headers["x-user-agent"]="contentstack-delivery-dotnet/2.23.0"; request.Timeout = timeout; if (proxy != null) diff --git a/Contentstack.Core/Models/AssetLibrary.cs b/Contentstack.Core/Models/AssetLibrary.cs index adb01dd..8ec17e8 100644 --- a/Contentstack.Core/Models/AssetLibrary.cs +++ b/Contentstack.Core/Models/AssetLibrary.cs @@ -316,6 +316,35 @@ public AssetLibrary Limit(int number) return this; } + /// + /// Filter assets that have specific tags. + /// + /// Array of tags to filter assets by. + /// Current instance of AssetLibrary, this will be useful for a chaining calls. + /// + /// + /// ContentstackClient stack = new ContentstackClinet("api_key", "delivery_token", "environment"); + /// AssetLibrary assetLibrary = stack.AssetLibrary(); + /// assetLibrary.Tags(new string[] {"image", "banner"}); + /// ContentstackCollection contentstackCollection = await assetLibrary.FetchAll(); + /// + /// + public AssetLibrary Tags(string[] tags) + { + try + { + if (tags != null && tags.Length > 0) + { + UrlQueries["tags"] = tags; + } + } + catch (Exception e) + { + throw new Exception(StackConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + /// /// Specifies an array of 'only' keys in BASE object that would be 'included' in the response. /// diff --git a/Directory.Build.props b/Directory.Build.props index cff023c..4f68ca3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 2.22.2 + 2.23.0