diff --git a/.gitignore b/.gitignore
index e940f15..52c1b1e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,4 +21,48 @@ packages/
*.trx
*/TestResults/
*/app.config
-*/LivePreviewTest.cs
\ No newline at end of file
+*/LivePreviewTest.cs
+# Security - Exclude ALL configuration files with credentials
+App.config.local
+*.config.local
+**/App.config
+**/app.config
+Contentstack.Core.Tests/App.config
+Contentstack.Core.Tests/app.config
+
+# Test Results
+TestResults/
+test-report*.html
+test-report-enhanced*.html
+*.trx
+docs
+
+# Security Scan Reports
+SECURITY-SCAN-REPORT.txt
+
+# IDE and OS files
+.DS_Store
+.vs/
+.vscode/
+*.swp
+*.swo
+*~
+
+# Build artifacts
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# NuGet
+*.nupkg
+*.snupkg
+.nuget/
+packages/
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
diff --git a/.talismanrc b/.talismanrc
index 6cfd212..6f1f627 100644
--- a/.talismanrc
+++ b/.talismanrc
@@ -25,4 +25,103 @@ fileignoreconfig:
- filename: Contentstack.Core.Tests/RegionHandlerTest.cs
checksum: 69899138754908e156aa477d775d12fd6b3fefc1a6c2afec22cb409bd6e6446c
- filename: CHANGELOG.md
- checksum: bc17fd4cf564e524c686a8271033f8e6e7f5f69de8137007d1c72d5f563fe92a
\ No newline at end of file
+ checksum: bc17fd4cf564e524c686a8271033f8e6e7f5f69de8137007d1c72d5f563fe92a
+- filename: Contentstack.Core.Tests/Helpers/RequestLoggingPlugin.cs
+ checksum: 8814a9c304834162cf27c214ef9d13dce4d9b458dd55a9c68d7bb48607cd87c4
+- filename: Contentstack.Core.Tests/Helpers/TestAssert.cs
+ checksum: 90f457e5ae4c6955022ea60c4a577099e78d3c6fded31dbdd0817a004c3941d8
+- filename: Contentstack.Core.Tests/Helpers/AssertionHelper.cs
+ checksum: 449fd092f0f5229653ec8d5c7c0c4bb74df96ef14a71183aa78f35f988fad64a
+- filename: Contentstack.Core.Tests/Helpers/IntegrationTestBase.cs
+ checksum: fe8a38f19e916767f6a016b33d4e8cd12316aaf70a8a46bcd6e44fedcbe921d7
+- filename: Contentstack.Core.Tests/Helpers/TestOutputHelper.cs
+ checksum: fa038ae6aa535d6684bc545c1a1ea174a572128f1ea40c08ef81637920a2007a
+- filename: Contentstack.Core.Tests/Helpers/TestDataHelper.cs
+ checksum: 67c8afb436287676e0db3a62a9213d800239cf5bb543cc4d81f438655abf0e1f
+- filename: Contentstack.Core.Tests/Integration/ClientTests/ContentstackClientTest.cs
+ checksum: 05fa29ba19d2eadb8090dec72f768a5d9edcc3a26cb746817f502093dee89987
+- filename: Contentstack.Core.Tests/Integration/StackTests/StackOperationsComprehensiveTest.cs
+ checksum: bb169d53e19f32fd08b731749ad161366af68d0618fd025ac571b2acfc730966
+- filename: Contentstack.Core.Tests/Integration/ConfigurationTests/RegionSupportTest.cs
+ checksum: fd424a7595c59a0d5b48d317f70d9be2646e5b246bcae865c5de391701e8b1dd
+- filename: Contentstack.Core.Tests/Integration/ModularBlocksTests/ModularBlocksComprehensiveTest.cs
+ checksum: 17a32d5d99819b4d00a6ab786484322640accc619936564e2fa5de060b2304d2
+- filename: Contentstack.Core.Tests/Integration/PaginationTests/PaginationComprehensiveTest.cs
+ checksum: c6eb3ac1e2205d15e18dc9c12252acc3d628c0c951e7dde72e8340873b499b0b
+- filename: Contentstack.Core.Tests/Integration/CachingTests/CachePersistenceTest.cs
+ checksum: d7f0535970f08ddeab8fc11bf81ec90da3e60ab80debb2bea020c59a2fe1a2c6
+- filename: Contentstack.Core.Tests/Integration/ConfigurationTests/TimeoutConfigurationTest.cs
+ checksum: 3b4767fcb027b050bc563694c4c9272125041b7c586d18a9f8afa70d4c97528a
+- filename: Contentstack.Core.Tests/Integration/ConfigurationValidationTest.cs
+ checksum: 59af67d70f9855948e77bbe21248c8c9815b01b325d4dcd731e7e078134f8648
+- filename: Contentstack.Core.Tests/Integration/LocalizationTests/LocalizationExtendedTest.cs
+ checksum: 52d3818f79d12e6dc898f1cd5133d409022ba4523a8586a2dd7cd25b960cf82a
+- filename: Contentstack.Core.Tests/Integration/AssetTests/AssetManagementComprehensiveTest.cs
+ checksum: be791ec38c5264393d254c7508079847da24f840b28699713945a06b80e1397f
+- filename: Contentstack.Core.Tests/Integration/PerformanceTests/PerformanceLargeDatasetsTest.cs
+ checksum: 59c1da08afe8dbb03b159fb19129a09ffd7b550137cf00606ed58c99baa34dbd
+- filename: Contentstack.Core.Tests/Integration/QueryTests/ComplexFieldQueriesTest.cs
+ checksum: ccc1ecf1210563f566e7153d26a313d1ecc3f7e349073cb2a65b79afb770baf5
+- filename: Contentstack.Core.Tests/Integration/QueryTests/AdvancedQueryFeaturesTest.cs
+ checksum: ef5698ba3e1a2bb4ae35d0f1336df05d13b033ab3a90b44a12e004fa4920660e
+- filename: Contentstack.Core.Tests/Integration/BranchTests/MetadataBranchComprehensiveTest.cs
+ checksum: 9ef81b199971e8b92a2a3703ba4760169ff578d536bd9e21441b9941e5564894
+- filename: Contentstack.Core.Tests/Integration/QueryEncodingTests/QueryEncodingComprehensiveTest.cs
+ checksum: 007ffe1e5582f4bbc5443ec4b2866ebaf8add8cda3d531e0bafb25220fe558f1
+- filename: Contentstack.Core.Tests/Integration/QueryTests/EntryQueryablesComprehensiveTest.cs
+ checksum: bb1a1fe53b751e7b6f5cd595685b0688592932ce857cec56b69dcd7b36531354
+- filename: Contentstack.Core.Tests/Integration/QueryTests/QueryIncludeExtendedTest.cs
+ checksum: e34521bff26d14fc6d793b9bb17be446b638c0f47496b25c8b5830c82e71e5f3
+- filename: Contentstack.Core.Tests/Integration/RetryTests/RetryIntegrationTest.cs
+ checksum: 67c2c3d3884b097c773cb1bbfcaad980e564da9d347eadef03ee6e6a886c5ba1
+- filename: Contentstack.Core.Tests/Integration/SyncTests/ExtendedSyncApiTest.cs
+ checksum: e1ccde67996299b12208442e20464cdef12586da54db1368b7e312d285dd214d
+- filename: Contentstack.Core.Tests/Integration/SyncTests/SyncApiComprehensiveTest.cs
+ checksum: 689e125c4fb79e1fe0284e34b23e9d07dbfc05a077e9028c739d421108f45d47
+- filename: Contentstack.Core.Tests/Integration/ReferenceTests/MultiReferenceTest.cs
+ checksum: 6b24eafdffb7fcb92d0e118b1092c151fed75505fc50bb138d706a51aed606d7
+- filename: Contentstack.Core.Tests/Integration/VariantsTests/EntryVariantsComprehensiveTest.cs
+ checksum: 5c16d15ac80dfcec8b85b67c83bcd2e962b365a95f4a8553f52fc3bc22d15fee
+- filename: Contentstack.Core.Tests/Integration/ContentTypeTests/ContentTypeOperationsTest.cs
+ checksum: dcda4e54f4532a3c24c67cbb22030e8a565cd62fa8ed1f7fdc84d59f48354e51
+- filename: Contentstack.Core.Tests/Integration/Taxonomy/TaxonomySupportTest.cs
+ checksum: 44add94c65a619f943426181346b503eff1cdf5e6f3cd081fd03ac4466b33291
+- filename: Contentstack.Core.Tests/Integration/ReferenceTests/DeepReferencesComprehensiveTest.cs
+ checksum: ab0e55eb40a4a05cdc4adbe5e2135aac2022b2d2823c12c8c9b6221874dac7ce
+- filename: Contentstack.Core.Tests/Integration/QueryTests/QueryOperatorsComprehensiveTest.cs
+ checksum: 3d564267e45787951231381fd074b1331603ff5d673639f8fe99115299d2acda
+- filename: Contentstack.Core.Tests/Integration/QueryTests/ComplexQueryCombinationsTest.cs
+ checksum: cb1379e0e4824d1b1566114a8240836347667943006614c825dd042da40b0f9e
+- filename: Contentstack.Core.Tests/Integration/ContentTypeTests/ContentTypeQueryTest.cs
+ checksum: 2ddcb8884f4a224ab16fa393f689ec6f8855159b3d52b63eb19c5524f2d5712c
+- filename: Contentstack.Core.Tests/Integration/EntryTests/EntryIncludeExtendedTest.cs
+ checksum: cdd92a05886e84235814eadb8dad2d1dedc1ae3f7bcd03ae7925d790fc964ad9
+- filename: Contentstack.Core.Tests/Integration/MetadataTests/IncludeMetadataComprehensiveTest.cs
+ checksum: c67730d830b66b266937b9ad81c2fe4500455f23f72326d1c8478a15224076ec
+- filename: Contentstack.Core.Tests/Integration/EntryTests/EntryOperationsComprehensiveTest.cs
+ checksum: 76c3cebeb144aa2787576df9590a457f90dda35489a30e316e62be6a60fde13e
+- filename: Contentstack.Core.Tests/Integration/ErrorHandling/ErrorHandlingComprehensiveTest.cs
+ checksum: 151e118f345090348bdad69f44cce09692f1fe705b8fe7045b8532074030829d
+- filename: Contentstack.Core.Tests/Integration/HeaderTests/HeaderManagementTest.cs
+ checksum: d086345f0f0301ec3000e5229a40e5817fdb1eee969ec6078a1e6f20890661f0
+- filename: Contentstack.Core.Tests/Integration/EntryTests/FieldProjectionAndReferencesTest.cs
+ checksum: 86662ca65ce88a5d2bd756d88018ae89d9ccdafed6380862be37fc72ba7cece5
+- filename: Contentstack.Core.Tests/Integration/GlobalFieldsTests/GlobalFieldsComprehensiveTest.cs
+ checksum: b863408ced3d5e7dcf404600e36d7f554726180d9dca3075a1e4639769a01d55
+- filename: Contentstack.Core.Tests/Integration/ImageDeliveryTests/ImageDeliveryComprehensiveTest.cs
+ checksum: fae01875ff7bd3ab2cdcdfbac6dc94f5f358e8832a1f2ede96af63b3557488cd
+- filename: Contentstack.Core.Tests/Integration/GlobalFieldsTests/NestedGlobalFieldsTest.cs
+ checksum: 99893e7d8b17ac2e9c6675aa0ae2ac8200bc2f3e9639ccaf853c97b26173f446
+- filename: Contentstack.Core.Tests/Integration/LocalizationTests/LocaleFallbackChainTest.cs
+ checksum: 382e1873b74685a8c62a73e627668bd354e1be91530bcf43c0782e28856fdd93
+- filename: Contentstack.Core.Tests/Integration/JSONRTETests/JsonRteEmbeddedItemsTest.cs
+ checksum: 43aa302af75031f4621de1287dbcdaa63151659230f20a0a785cc0dd5be0e1c4
+- filename: Contentstack.Core.Tests/Integration/LivePreview/LivePreviewBasicTest.cs
+ checksum: 01517f2224fbb2956d79292e6d3d23d1cc970dbfc190623496bcac1335bcd683
+- filename: Contentstack.Core.Tests/generate_html_report.py
+ checksum: b4bec9ef853703e989b3d8077edc5c3ec6ea13a23826699d8beca5e87323e128
+- filename: Scripts/generate_html_report.py
+ checksum: 343a6c4a3608e4506cd7c9de04f9246da304ff95d256a3215c2f0a2d37d4e4da
+- filename: Scripts/generate_enhanced_html_report.py
+ checksum: 69de208724714fcb474e41e17c5e67a1f875b96e2cc479c71f03c38b7a8c3be9
+version: "1.0"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 776e063..62e0336 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+### Version: 2.26.0
+#### Date: Feb-10-2026
+
+##### Feat:
+- CDA / – AssetFields support
+ - Added `AssetFields(params string[] fields)` to request specific asset-related metadata via the CDA `asset_fields[]` query parameter
+ - Implemented on: Entry (single entry fetch), Query (entries find), Asset (single asset fetch), AssetLibrary (assets find)
+ - Valid parameters: `user_defined_fields`, `embedded_metadata`, `ai_generated_metadata`, `visual_markups`
+ - Method is chainable; when called with no arguments, the query parameter is not set
+- CDA / – Asset localisation support
+ - Added `SetLocale(string locale)` on Asset for single-asset fetch by locale (e.g. `stack.Asset(uid).SetLocale("en-us").Fetch()`)
+ - Added `Title` property on Asset for localised title in API response
+ - AssetLibrary `SetLocale` continues to support listing assets by locale
+
### Version: 2.25.2
#### Date: Nov-13-2025
diff --git a/Contentstack.AspNetCore/Contentstack.AspNetCore.csproj b/Contentstack.AspNetCore/Contentstack.AspNetCore.csproj
index b71bafa..71b4a06 100644
--- a/Contentstack.AspNetCore/Contentstack.AspNetCore.csproj
+++ b/Contentstack.AspNetCore/Contentstack.AspNetCore.csproj
@@ -8,7 +8,7 @@
Contentstack
$(Version)
Main release
- Copyright (c) 2012-2025 Contentstack (http://app.contentstack.com). All Rights Reserved
+ Copyright (c) 2012-2026 Contentstack (http://app.contentstack.com). All Rights Reserved
https://github.com/contentstack/contentstack-dotnet
v$(Version)
$(Version)
diff --git a/Contentstack.Core.Tests/AssetTagsBasicTest.cs b/Contentstack.Core.Tests/AssetTagsBasicTest.cs
deleted file mode 100644
index f82ef00..0000000
--- a/Contentstack.Core.Tests/AssetTagsBasicTest.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-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
deleted file mode 100644
index 6f779da..0000000
--- a/Contentstack.Core.Tests/AssetTest.cs
+++ /dev/null
@@ -1,955 +0,0 @@
-using System;
-using Xunit;
-using Contentstack.Core.Models;
-using System.Threading.Tasks;
-using System.Collections.Generic;
-using System.Linq;
-using Newtonsoft.Json.Linq;
-
-namespace Contentstack.Core.Tests
-{
- public class AssetTest
- {
-
- ContentstackClient client = StackConfig.GetStack();
-
- public async Task FetchAssetUID()
- {
- AssetLibrary assetLibrary = client.AssetLibrary();
- ContentstackCollection assets = await assetLibrary.FetchAll();
- Assert.True(assets.Count() > 0);
- return assets.First().Uid;
- }
-
- [Fact]
- public async Task FetchAssetByUid()
- {
- string uid = await FetchAssetUID();
- Asset asset = client.Asset(uid);
- await asset.Fetch().ContinueWith((t) =>
- {
- Asset result = t.Result;
- if (result == null)
- {
- Assert.Fail( "Entry.Fetch is not match with expected result.");
- }
- else
- {
- Assert.True(result.FileName.Length > 0);
- }
- });
- }
-
- [Fact]
- public async Task FetchAssetToAccessAttributes()
- {
- string uid = await FetchAssetUID();
- Asset a1 = await client.Asset(uid).AddParam("include_dimension", "true").Fetch();
- Assert.NotEmpty(a1.Url);
- Assert.NotEmpty(a1.ContentType);
- Assert.NotEmpty(a1.Version);
- Assert.NotEmpty(a1.FileSize);
- Assert.NotEmpty(a1.FileName);
- Assert.NotEmpty(a1.Description);
- Assert.NotEmpty(a1.UpdatedBy);
- Assert.NotEmpty(a1.CreatedBy);
- Assert.NotEmpty(a1.PublishDetails);
- }
-
- [Fact]
- public async Task FetchAssetsPublishFallback()
- {
- List list = new List();
- list.Add("en-us");
- list.Add("ja-jp");
- ContentstackCollection assets = await client.AssetLibrary()
- .SetLocale("ja-jp")
- .IncludeFallback()
- .FetchAll();
- ;
- Assert.True(assets.Items.Count() > 0);
- foreach (Asset asset in assets)
- {
- Assert.Contains((string)(asset.Get("publish_details") as JObject).GetValue("locale"), list);
- }
- }
-
- [Fact]
- public async Task FetchAssetsPublishWithoutFallback()
- {
- List list = new List();
- list.Add("ja-jp");
- ContentstackCollection assets = await client.AssetLibrary()
- .SetLocale("ja-jp")
- .FetchAll();
- ;
- Assert.True(assets.Items.Count() > 0);
- foreach (Asset asset in assets)
- {
- Assert.Contains((string)(asset.Get("publish_details") as JObject).GetValue("locale"), list);
- }
- }
-
- [Fact]
- public async Task FetchAssets()
- {
- AssetLibrary assetLibrary = client.AssetLibrary();
- ContentstackCollection assets = await assetLibrary.FetchAll();
- Assert.True(assets.Count() > 0);
- foreach (Asset asset in assets)
- {
- Assert.True(asset.FileName.Length > 0);
- }
- }
-
- [Fact]
- public async Task FetchAssetsOrderByAscending()
- {
- AssetLibrary assetLibrary = client.AssetLibrary();
- assetLibrary.SortWithKeyAndOrderBy("created_at", Internals.OrderBy.OrderByAscending);
- ContentstackCollection assets = await assetLibrary.FetchAll();
- Assert.True(assets.Count() > 0);
- DateTime dateTime = new DateTime();
- foreach (Asset asset in assets)
- {
- if (dateTime != null)
- {
- if (dateTime.CompareTo(asset.GetCreateAt()) != -1 && dateTime.CompareTo(asset.GetCreateAt()) != 0)
- {
- Assert.Fail();
- }
- }
- dateTime = asset.GetCreateAt();
- Assert.True(asset.FileName.Length > 0);
- }
- }
-
- [Fact]
- public async Task FetchAssetsIncludeRelativeURL()
- {
- AssetLibrary assetLibrary = client.AssetLibrary();
- assetLibrary.IncludeRelativeUrls();
- ContentstackCollection assets = await assetLibrary.FetchAll();
- Assert.True(assets.Count() > 0);
- foreach (Asset asset in assets)
- {
- Assert.DoesNotContain(asset.Url, "http");
- Assert.True(asset.FileName.Length > 0);
- }
- }
-
- [Fact]
- public async Task FetchAssetWithQuery()
- {
- JObject queryObject = new JObject
- {
- { "filename", "image3.png" }
- };
- ContentstackCollection assets = await client.AssetLibrary().Query(queryObject).FetchAll();
- Assert.True(assets.Count() > 0);
- foreach (Asset asset in assets)
- {
- Assert.DoesNotContain(asset.Url, "http");
- Assert.True(asset.FileName.Length > 0);
- }
- }
-
- [Fact]
- public async Task FetchAssetCountAsync()
- {
- AssetLibrary assetLibrary = client.AssetLibrary().
- IncludeMetadata().SetLocale("en-us");
- JObject jObject = await assetLibrary.Count();
- if (jObject == null)
- {
- Assert.Fail( "Query.Exec is not match with expected result.");
- }
- else if (jObject != null)
- {
- Assert.Equal(5, jObject.GetValue("assets"));
- }
- else
- {
- Assert.Fail( "Result doesn't mathced the count.");
- }
- }
-
- [Fact]
- public async Task FetchAssetSkipLimit()
- {
- AssetLibrary assetLibrary = client.AssetLibrary().SetLocale("en-us").Skip(2).Limit(5);
- ContentstackCollection assets = await assetLibrary.FetchAll();
- if (assets == null)
- {
- Assert.Fail( "Query.Exec is not match with expected result.");
- }
- else if (assets != null)
- {
- Assert.Equal(3, assets.Items.Count());
- }
- else
- {
- Assert.Fail( "Result doesn't mathced the count.");
- }
- }
-
- [Fact]
- public async Task FetchAssetOnly()
- {
- AssetLibrary assetLibrary = client.AssetLibrary().Only(new string[] { "url"});
- ContentstackCollection assets = await assetLibrary.FetchAll();
- if (assets == null)
- {
- Assert.Fail( "Query.Exec is not match with expected result.");
- }
- else if (assets != null)
- {
- foreach (Asset asset in assets)
- {
- Assert.DoesNotContain(asset.Url, "http");
- Assert.Null(asset.Description);
- Assert.Null(asset.FileSize);
- Assert.Null(asset.Tags);
- Assert.Null(asset.Description);
- }
- }
- else
- {
- Assert.Fail( "Result doesn't mathced the count.");
- }
- }
-
- [Fact]
- public async Task FetchAssetExcept()
- {
- AssetLibrary assetLibrary = client.AssetLibrary().Except(new string[] { "description" });
- ContentstackCollection assets = await assetLibrary.FetchAll();
- if (assets == null)
- {
- Assert.Fail( "Query.Exec is not match with expected result.");
- }
- else if (assets != null)
- {
- foreach (Asset asset in assets)
- {
- Assert.DoesNotContain(asset.Url, "http");
- Assert.Null(asset.Description);
- }
- }
- else
- {
- Assert.Fail( "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);
- }
- }
- }
- }
-
- [Fact]
- public void Query_MultipleCalls_ShouldMergeQueries_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
- JObject firstQuery = new JObject
- {
- { "filename", "test1.png" },
- { "content_type", "image/png" }
- };
- JObject secondQuery = new JObject
- {
- { "file_size", 1024 },
- { "tags", new JArray { "test", "image" } }
- };
-
- // Act
- var result = assetLibrary.Query(firstQuery).Query(secondQuery);
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- // The method should not throw an exception when called multiple times
- }
-
- [Fact]
- public void Query_SingleCall_ShouldWorkAsBefore_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
- JObject queryObject = new JObject
- {
- { "filename", "test.png" }
- };
-
- // Act
- var result = assetLibrary.Query(queryObject);
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- }
-
- [Fact]
- public void Query_WithEmptyObject_ShouldNotThrowException_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
- JObject emptyQuery = new JObject();
-
- // Act & Assert
- var result = assetLibrary.Query(emptyQuery);
- Assert.NotNull(result);
- Assert.IsType(result);
- }
-
- [Fact]
- public void Query_WithNullValues_ShouldHandleGracefully_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
- JObject queryWithNulls = new JObject
- {
- { "filename", "test.png" },
- { "null_field", null }
- };
-
- // Act & Assert
- var result = assetLibrary.Query(queryWithNulls);
- Assert.NotNull(result);
- Assert.IsType(result);
- }
-
- [Fact]
- public void Query_ChainedWithOtherMethods_ShouldWork_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
- JObject queryObject = new JObject
- {
- { "filename", "test.png" }
- };
-
- // Act
- var result = assetLibrary
- .Query(queryObject)
- .Limit(10)
- .Skip(0)
- .IncludeMetadata();
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- }
-
- [Fact]
- public void Query_MultipleCallsWithSameKeys_ShouldMergeValues_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
- JObject firstQuery = new JObject
- {
- { "tags", new JArray { "tag1", "tag2" } }
- };
- JObject secondQuery = new JObject
- {
- { "tags", new JArray { "tag3", "tag4" } }
- };
-
- // Act
- var result = assetLibrary.Query(firstQuery).Query(secondQuery);
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- // The method should handle merging arrays without throwing exceptions
- }
-
- [Fact]
- public void Query_WithComplexNestedObjects_ShouldMergeCorrectly_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
- JObject firstQuery = new JObject
- {
- { "metadata", new JObject
- {
- { "author", "John Doe" },
- { "version", 1 }
- }
- }
- };
- JObject secondQuery = new JObject
- {
- { "metadata", new JObject
- {
- { "department", "IT" }
- }
- },
- { "filename", "test.png" }
- };
-
- // Act
- var result = assetLibrary.Query(firstQuery).Query(secondQuery);
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- }
-
- [Fact]
- public void Where_SingleCall_ShouldAddKeyValuePair_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
- string key = "filename";
- string value = "test.png";
-
- // Act
- var result = assetLibrary.Where(key, value);
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- }
-
- [Fact]
- public void Where_MultipleCalls_ShouldAddMultipleKeyValuePairs_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
-
- // Act
- var result = assetLibrary
- .Where("filename", "test.png")
- .Where("content_type", "image/png")
- .Where("file_size", "1024");
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- }
-
- [Fact]
- public void Where_WithEmptyStrings_ShouldHandleGracefully_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
-
- // Act & Assert
- var result = assetLibrary.Where("", "");
- Assert.NotNull(result);
- Assert.IsType(result);
- }
-
- [Fact]
- public void Where_WithNullKey_ShouldHandleGracefully_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
-
- // Act & Assert
- var result = assetLibrary.Where(null, "value");
- Assert.NotNull(result);
- Assert.IsType(result);
- }
-
- [Fact]
- public void Where_WithNullValue_ShouldHandleGracefully_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
-
- // Act & Assert
- var result = assetLibrary.Where("key", null);
- Assert.NotNull(result);
- Assert.IsType(result);
- }
-
- [Fact]
- public void Where_ChainedWithOtherMethods_ShouldWork_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
-
- // Act
- var result = assetLibrary
- .Where("filename", "test.png")
- .Limit(10)
- .Skip(0)
- .IncludeMetadata();
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- }
-
- [Fact]
- public void Where_WithQueryMethod_ShouldWorkTogether_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
- JObject queryObject = new JObject
- {
- { "content_type", "image/png" }
- };
-
- // Act
- var result = assetLibrary
- .Query(queryObject)
- .Where("filename", "test.png")
- .Where("file_size", "1024");
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- }
-
- [Fact]
- public void Where_OverwritesExistingKey_ShouldReplaceValue_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
-
- // Act
- var result = assetLibrary
- .Where("filename", "original.png")
- .Where("filename", "updated.png");
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- }
-
- [Fact]
- public void Where_WithSpecialCharacters_ShouldHandleCorrectly_Test()
- {
- // Arrange
- AssetLibrary assetLibrary = client.AssetLibrary();
-
- // Act
- var result = assetLibrary
- .Where("file_name", "test-file_123.png")
- .Where("description", "File with special chars: @#$%");
-
- // Assert
- Assert.NotNull(result);
- Assert.IsType(result);
- }
- }
-}
\ No newline at end of file
diff --git a/Contentstack.Core.Tests/ContentTypeTest.cs b/Contentstack.Core.Tests/ContentTypeTest.cs
deleted file mode 100644
index d937634..0000000
--- a/Contentstack.Core.Tests/ContentTypeTest.cs
+++ /dev/null
@@ -1,210 +0,0 @@
-using System;
-using Xunit;
-using Contentstack.Core.Models;
-using Contentstack.Core.Internals;
-using System.Threading.Tasks;
-using System.Collections.Generic;
-using Newtonsoft.Json.Linq;
-
-namespace Contentstack.Core.Tests
-{
- public class ContentTypeTest
-
- {
- ContentstackClient client = StackConfig.GetStack();
- String source = "source";
-
- [Fact]
- public async Task FetchContenTypeSchema()
- {
- ContentType contenttype = client.ContentType(source);
-
- var result = await contenttype.Fetch();
- if (result == null)
- {
- Assert.Fail( "contenttype.FetchSchema() is not match with expected result.");
- }
- else
- {
- Assert.True(true);
- }
- }
-
- [Fact]
- public async Task FetchContenTypeSchemaIncludeGlobalFields()
- {
- ContentType contenttype = client.ContentType(source);
- var param = new Dictionary();
- param.Add("include_global_field_schema", true);
- var result = await contenttype.Fetch(param);
- if (result == null)
- {
- Assert.Fail( "contenttype.FetchSchema() is not match with expected result.");
- }
- else
- {
- Assert.True(true);
- }
- }
-
- [Fact]
- public async Task GetContentTypes()
- {
- var result = await client.GetContentTypes();
-
- if (result == null)
- {
- Assert.Fail( "client.getContentTypes is not match with expected result.");
- }
- else
- {
- Assert.True(true);
-
- }
- }
-
- [Fact]
- public async Task GetContentTypesIncludeGlobalFields()
- {
- var param = new Dictionary();
- param.Add("include_global_field_schema", true);
-
- var result = await client.GetContentTypes(param);
-
- if (result == null)
- {
- Assert.Fail( "client.getContentTypes is not match with expected result.");
- }
- else
- {
- Assert.True(true);
-
- }
- }
-
- [Fact]
- public async Task FetchGlobalFieldSchema()
- {
- string globalFieldUid = "global_field_uid";
- GlobalField globalField = client.GlobalField(globalFieldUid);
-
- var result = await globalField.Fetch();
- Assert.NotNull(result);
- Assert.True(result.HasValues, "GlobalField.Fetch() did not return expected schema.");
- }
-
- [Fact]
- public async Task FetchGlobalFieldSchema_InvalidUid_ThrowsOrReturnsNull()
- {
- string invalidUid = "invalid_uid";
- GlobalField globalField = client.GlobalField(invalidUid);
- await Assert.ThrowsAnyAsync(async () => await globalField.Fetch());
- }
-
- [Fact]
- public async Task FetchGlobalFieldSchema_WithParameters_ReturnsSchema()
- {
- string globalFieldUid = "global_field_uid";
- GlobalField globalField = client.GlobalField(globalFieldUid);
- var param = new Dictionary { { "include_global_field_schema", true } };
- var result = await globalField.Fetch(param);
- Assert.NotNull(result);
- Assert.True(result.HasValues, "GlobalField.Fetch() with params did not return expected schema.");
- }
-
- [Fact]
- public void SetAndRemoveHeader_WorksCorrectly()
- {
- string globalFieldUid = "global_field_uid";
- GlobalField globalField = client.GlobalField(globalFieldUid);
- globalField.SetHeader("custom_key", "custom_value");
- globalField.RemoveHeader("custom_key");
- Assert.True(true);
- }
-
- [Fact]
- public async Task FetchGlobalFieldSchema_WithCustomHeader()
- {
- string globalFieldUid = "global_field_uid";
- GlobalField globalField = client.GlobalField(globalFieldUid);
- globalField.SetHeader("custom_key", "custom_value");
- var result = await globalField.Fetch();
- Assert.NotNull(result);
- }
-
- [Fact]
- public async Task FetchGlobalFieldSchema_NullParameters_Succeeds()
- {
- string globalFieldUid = "global_field_uid";
- GlobalField globalField = client.GlobalField(globalFieldUid);
- var result = await globalField.Fetch(null);
- Assert.NotNull(result);
- }
-
- [Fact]
- public void GlobalField_EmptyUid_Throws()
- {
- Assert.Throws(() => {
- GlobalField globalField = client.GlobalField("");
- });
- }
-
- [Fact]
- public async Task GlobalFieldQuery_Find_ReturnsArray()
- {
- var query = client.GlobalFieldQuery();
- var result = await query.Find();
-
- Assert.NotNull(result);
- }
-
- [Fact]
- public async Task GlobalFieldQuery_Find_WithParameters_ReturnsArray()
- {
- var query = client.GlobalFieldQuery();
- var param = new Dictionary { { "include_global_field_schema", true } };
- var result = await query.Find(param);
- Assert.NotNull(result);
- }
-
- [Fact]
- public async Task GlobalFieldQuery_Find_WithSkipAndLimit_ReturnsArray()
- {
- var query = client.GlobalFieldQuery();
- var param = new Dictionary { { "skip", 1 }, { "limit", 2 } };
- var result = await query.Find(param);
- Assert.Empty(result["global_fields"]);
- }
-
- [Fact]
- public void GlobalFieldQuery_IncludeBranch_SetsQueryParam()
- {
- var query = client.GlobalFieldQuery();
- var result = query.IncludeBranch();
- Assert.NotNull(result);
- Assert.Equal(query, result);
- }
-
- [Fact]
- public void GlobalFieldQuery_IncludeGlobalFieldSchema_SetsQueryParam()
- {
- var query = client.GlobalFieldQuery();
- var result = query.IncludeGlobalFieldSchema();
- Assert.NotNull(result);
- }
-
- [Fact]
- public async Task GlobalFieldQuery_Find_InvalidParams_ThrowsOrReturnsEmpty()
- {
- var query = client.GlobalFieldQuery();
- var invalidParams = new Dictionary { { "invalid_param", true } };
-
- var result = await query.Find(invalidParams);
-
- Assert.NotNull(result);
- Assert.IsType(result);
- var globalFields = result["global_fields"] as JArray;
- Assert.NotNull(globalFields);
- }
- }
-}
\ No newline at end of file
diff --git a/Contentstack.Core.Tests/Contentstack.Core.Tests.csproj b/Contentstack.Core.Tests/Contentstack.Core.Tests.csproj
index c40482e..42960a4 100644
--- a/Contentstack.Core.Tests/Contentstack.Core.Tests.csproj
+++ b/Contentstack.Core.Tests/Contentstack.Core.Tests.csproj
@@ -28,6 +28,7 @@
+
@@ -59,4 +60,9 @@
+
+
+ Always
+
+
diff --git a/Contentstack.Core.Tests/EntryTest.cs b/Contentstack.Core.Tests/EntryTest.cs
deleted file mode 100644
index 73fc79a..0000000
--- a/Contentstack.Core.Tests/EntryTest.cs
+++ /dev/null
@@ -1,461 +0,0 @@
-using System;
-using Xunit;
-using Contentstack.Core.Models;
-using System.Threading.Tasks;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text.RegularExpressions;
-using System.Reflection;
-using Contentstack.Core.Tests.Models;
-using Contentstack.Core.Internals;
-using Newtonsoft.Json.Linq;
-
-namespace Contentstack.Core.Tests
-{
-
- public class EntryTest
- {
- ContentstackClient client = StackConfig.GetStack();
-
- ////PROD STAG
- String source = "source";
- String singelEntryFetchUID = "";
- string htmlSource = "";
- String referenceFieldUID = "reference";
- //EU
- //String source = "source";
- //String singelEntryFetchUID = "bltf4268538a14fc5e1";
- //string htmlSource = "blt7c4197d43c1156ba";
- //String referenceFieldUID = "reference";
- public async Task GetUID(string title)
- {
- Query query = client.ContentType(source).Query();
- var result = await query.Find();
- if (result != null)
- {
- foreach (var data in result.Items)
- {
- if (data.Title == title)
- {
- return data.Uid;
- }
- }
- }
-
- return null;
- }
-
- [Fact]
- public async Task FetchByUid() {
- ContentType contenttype = client.ContentType(source);
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
- sourceEntry.IncludeMetadata();
- await sourceEntry.Fetch().ContinueWith((t) =>
- {
- Entry result = t.Result;
- if (result == null)
- {
- Assert.Fail( "Entry.Fetch is not match with expected result.");
- }
- else
- {
- Assert.True(result.Uid == sourceEntry.Uid);
- }
- });
- }
-
- [Fact]
- public async Task FetchEntryByUIDPublishFallback()
- {
- List list = new List();
- list.Add("en-us");
- list.Add("ja-jp");
- ContentType contenttype = client.ContentType(source);
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
- sourceEntry = await sourceEntry
- .SetLocale("ja-jp")
- .IncludeFallback()
- .Fetch();
-
- Assert.Contains((string)(sourceEntry.Get("publish_details") as JObject).GetValue("locale"), list);
- }
-
- [Fact]
- public async Task FetchEntryByVariant()
- {
- ContentType contenttype = client.ContentType(source);
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
- await sourceEntry
- .Variant("variant1")
- .Fetch().ContinueWith((t) =>
- {
- Entry result = t.Result;
- if (result == null)
- {
- Assert.Fail( "Entry.Fetch is not match with expected result.");
- }
- else
- {
- Assert.True(result.Uid == sourceEntry.Uid);
- Assert.Null(result._variant);
- }
- });
- }
-
- [Fact]
- public async Task FetchEntryByVariants()
- {
- ContentType contenttype = client.ContentType(source);
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
- await sourceEntry
- .Variant(new List { "variant1", "variant2" })
- .Fetch().ContinueWith((t) =>
- {
- Entry result = t.Result;
- if (result == null)
- {
- Assert.Fail( "Entry.Fetch is not match with expected result.");
- }
- else
- {
- Assert.True(result.Uid == sourceEntry.Uid);
- Assert.Null(result._variant);
- }
- });
- }
-
- [Fact]
- public async Task FetchEntryByUIDPublishWithoutFallback()
- {
- List list = new List();
- list.Add("ja-jp");
- ContentType contenttype = client.ContentType(source);
- string uid = await GetUID("source1");
- Entry sourceEntry = await contenttype.Entry(uid)
- .SetLocale("ja-jp")
- .Fetch();
-
- Assert.Contains((string)(sourceEntry.Get("publish_details") as JObject).GetValue("locale"), list);
- }
-
- [Fact]
- public async Task IncludeReference() {
- ContentType contenttype = client.ContentType(source);
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- sourceEntry.IncludeReference(referenceFieldUID);
- var result = await sourceEntry.Fetch();
- if (result == null) {
- Assert.Fail( "Query.Exec is not match with expected result.");
- } else {
-
- bool IsTrue = false;
- List lstReference = result.Reference;
-
- if (lstReference.Count > 0) {
- IsTrue = lstReference.All(a => a is Entry);
- }
- Assert.True(IsTrue);
- }
- }
-
- [Fact]
- public async Task IncludeReferenceArray()
- {
- ContentType contenttype = client.ContentType(source);
-
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- sourceEntry.IncludeReference(new string[] {referenceFieldUID,"other_reference"});
- var result = await sourceEntry.Fetch();
- if (result == null)
- {
- Assert.Fail( "Query.Exec is not match with expected result.");
- }
- else
- {
- bool IsTrue = false;
- List> firstReference = result.Reference;
- List> secondReference = result.Other_reference;
- IsTrue = firstReference.All(a => a is Dictionary);
- Assert.True(IsTrue);
- IsTrue = secondReference.All(a => a is Dictionary);
- Assert.True(IsTrue);
- }
- }
-
- [Fact]
- public async Task Only() {
- ContentType contenttype = client.ContentType(source);
-
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- sourceEntry.Only(new string[] { "title", "number" });
- SourceModel result = await sourceEntry.Fetch();
- if (result == null) {
- Assert.Fail( "Query.Exec is not match with expected result.");
- } else {
-
- List uidKeys = new List() { "title", "number", "uid" };
- bool IsTrue = false;
- //IsTrue = data.Object.Keys.Count == 3 && data.Object.Keys.ToList().Contains(a=> ui);
- IsTrue = result.Uid != null && result.Title != null && result.Number == 4 ? true : false;
- Assert.True(IsTrue);
- }
- }
-
- [Fact]
- public async Task Except() {
- ContentType contenttype = client.ContentType(source);
-
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- sourceEntry.Except(new string[] { "title", "number" });
- var result = await sourceEntry.Fetch();
- if (result == null)
- {
- Assert.Fail( "Query.Exec is not match with expected result.");
- }
- else
- {
-
- List uidKeys = new List() { "title", "number" };
- bool IsTrue = false;
- IsTrue = result.Title == null && result.Number != 4.0 ? true : false;
- Assert.True(IsTrue);
- }
- }
-
- [Fact]
- public async Task GetCreateAt()
- {
- ContentType contenttype = client.ContentType(source);
-
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- var result = await sourceEntry.Fetch();
- var Created_at = result.Created_at;
- if (result == null)
- {
- Assert.Fail( "Query.Exec is not match with expected result.");
- }
- else
- {
- Assert.True(Created_at != default(DateTime));
- //Assert.True(true, "BuiltObject.Fetch is pass successfully.");
- }
- }
-
- [Fact]
- public async Task GetUpdateAt()
- {
- ContentType contenttype = client.ContentType(source);
-
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- var result = await sourceEntry.Fetch();
- var updated_at = result.updated_at;
- if (result == null)
- {
- Assert.Fail( "Query.Exec is not match with expected result.");
- }
- else
- {
- Assert.True(updated_at != default(DateTime));
- }
- }
-
- [Fact]
- public async Task GetCreatedBy()
- {
- ContentType contenttype = client.ContentType(source);
-
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- var result = await sourceEntry.Fetch();
- var created_by = result.created_by;
- if (created_by == null && created_by.Length == 0)
- {
- Assert.Fail( "Query.Exec is not match with expected result.");
- }
- else
- {
- Assert.True(created_by.Length > 0);
- //Assert.True(true, "BuiltObject.Fetch is pass successfully.");
- }
- }
-
- [Fact]
- public async Task GetUpdatedBy()
- {
- ContentType contenttype = client.ContentType(source);
-
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- var result = await sourceEntry.Fetch();
- var Updated_by = result.Updated_by;
- if (Updated_by == null && Updated_by.Length == 0)
- {
- Assert.Fail( "Query.Exec is not match with expected result.");
- }
- else
- {
- Assert.True(Updated_by.Length > 0);
- }
- }
-
- [Fact]
- public async Task GetTags()
- {
- ContentType contenttype = client.ContentType(source);
-
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- var result = await sourceEntry.Fetch();
- var Tags = result.Tags;
- if (Tags == null && Tags.Length == 0)
- {
- Assert.Fail( "Query.Exec is not match with expected result.");
- }
- else
- {
- Assert.True(Tags is object[] && Tags.Length > 0);
- }
- }
-
- [Fact]
- public async Task GetHTMLText()
- {
- ContentType contenttype = client.ContentType(source);
-
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- var result = await sourceEntry.Fetch();
-
-
- var HtmlText = result.GetHTMLText();
- if (string.IsNullOrEmpty(HtmlText) && HtmlText.Length == 0) {
- Assert.Fail( "Query.Exec is not match with expected result.");
- } else {
- var tagList = new List();
- string pattern = @"(?<=?)([^ >/]+)";
- var matches = Regex.Matches(HtmlText, pattern);
- for (int i = 0; i < matches.Count; i++)
- {
- tagList.Add(matches[i].ToString());
- }
- Assert.True(!string.IsNullOrEmpty(HtmlText) && HtmlText.Length > 0 && tagList.Count > 0);
- }
- }
-
- [Fact]
- public async Task IncludeMetadata()
- {
- ContentType contenttype = client.ContentType(source);
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- sourceEntry.IncludeMetadata();
- var result = await sourceEntry.Fetch();
-
- if (result == null)
- {
- Assert.Fail("Entry.Fetch is not match with expected result.");
- }
- else
- {
- // Verify metadata is included by checking if _metadata dictionary exists
- var metadata = result.GetMetadata();
- Assert.NotNull(metadata);
- // Metadata might be empty or might not contain "uid" - just verify it exists
- // The metadata property is populated when API returns _metadata in response
- Assert.True(true, "IncludeMetadata() was called and metadata property exists");
- }
- }
-
- [Fact(Skip = "Requires branch to be configured in Contentstack stack - set branch name in config")]
- public async Task IncludeBranch()
- {
- // This test requires a branch to be set up in your Contentstack stack
- // Update StackConfig to include branch name if needed
- ContentType contenttype = client.ContentType(source);
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- sourceEntry.IncludeBranch();
- var result = await sourceEntry.Fetch();
-
- if (result == null)
- {
- Assert.Fail("Entry.Fetch is not match with expected result.");
- }
- else
- {
- Assert.NotNull(result);
- // Branch information should be available in the response
- // The exact assertion depends on your data structure
- }
- }
-
- [Fact]
- public async Task IncludeOwner()
- {
- ContentType contenttype = client.ContentType(source);
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- sourceEntry.IncludeOwner();
- var result = await sourceEntry.Fetch();
-
- if (result == null)
- {
- Assert.Fail("Entry.Fetch is not match with expected result.");
- }
- else
- {
- Assert.NotNull(result);
- // Owner information should be available - verify created_by or updated_by fields
- Assert.NotNull(result.created_by);
- Assert.True(result.created_by.Length > 0);
- }
- }
-
- [Fact]
- public async Task GetMetadata()
- {
- ContentType contenttype = client.ContentType(source);
- string uid = await GetUID("source1");
- Entry sourceEntry = contenttype.Entry(uid);
-
- sourceEntry.IncludeMetadata();
- var result = await sourceEntry.Fetch();
-
- if (result == null)
- {
- Assert.Fail("Entry.Fetch is not match with expected result.");
- }
- else
- {
- var metadata = result.GetMetadata();
- Assert.NotNull(metadata);
- // Metadata might be empty - just verify GetMetadata() returns a valid dictionary
- // The actual content depends on what the API returns
- Assert.True(true, "GetMetadata() returns a valid dictionary (may be empty)");
- }
- }
- }
-}
diff --git a/Contentstack.Core.Tests/Helpers/AssertionHelper.cs b/Contentstack.Core.Tests/Helpers/AssertionHelper.cs
new file mode 100644
index 0000000..16ffe8b
--- /dev/null
+++ b/Contentstack.Core.Tests/Helpers/AssertionHelper.cs
@@ -0,0 +1,397 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+using Contentstack.Core.Models;
+using Contentstack.Core.Internals;
+using Contentstack.Core.Tests.Helpers;
+
+namespace Contentstack.Core.Tests.Helpers
+{
+ ///
+ /// Helper class for common test assertions
+ /// Provides reusable assertion logic to keep tests DRY
+ ///
+ public static class AssertionHelper
+ {
+ #region Entry Assertions
+
+ ///
+ /// Asserts that an entry has all basic required fields populated
+ ///
+ public static void AssertEntryBasicFields(Entry entry, string expectedUid = null)
+ {
+ TestAssert.NotNull(entry);
+ TestAssert.NotNull(entry.Uid);
+ TestAssert.NotEmpty(entry.Uid);
+
+ if (!string.IsNullOrEmpty(expectedUid))
+ {
+ TestAssert.Equal(expectedUid, entry.Uid);
+ }
+
+ // Title is usually required
+ TestAssert.NotNull(entry.Title);
+ }
+
+ ///
+ /// Asserts that an entry has metadata populated
+ ///
+ public static void AssertEntryMetadata(Entry entry)
+ {
+ TestAssert.NotNull(entry);
+
+ var metadata = entry.GetMetadata();
+ TestAssert.NotNull(metadata);
+
+ // Metadata should be a dictionary (even if empty)
+ TestAssert.IsType>(metadata);
+ }
+
+ ///
+ /// Asserts that a list of entries is not empty and valid
+ ///
+ public static void AssertEntriesValid(IEnumerable entries, int? expectedMinCount = null) where T : Entry
+ {
+ TestAssert.NotNull(entries);
+
+ var entriesList = entries.ToList();
+ TestAssert.NotEmpty(entriesList);
+
+ if (expectedMinCount.HasValue)
+ {
+ TestAssert.True(entriesList.Count >= expectedMinCount.Value,
+ $"Expected at least {expectedMinCount.Value} entries, but got {entriesList.Count}");
+ }
+
+ // All entries should have UIDs
+ TestAssert.All(entriesList, entry => TestAssert.NotNull(entry.Uid));
+ }
+
+ #endregion
+
+ #region Reference Assertions
+
+ ///
+ /// Asserts that references are populated at the specified level
+ ///
+ public static void AssertReferencesPopulated(Entry entry, string referenceFieldName, int expectedMinCount = 1)
+ {
+ TestAssert.NotNull(entry);
+
+ var referenceField = entry.Get(referenceFieldName);
+ TestAssert.NotNull(referenceField);
+
+ if (referenceField is List refList)
+ {
+ TestAssert.NotEmpty(refList);
+ TestAssert.True(refList.Count >= expectedMinCount,
+ $"Expected at least {expectedMinCount} references in '{referenceFieldName}', but got {refList.Count}");
+ TestAssert.All(refList, refEntry => TestAssert.NotNull(refEntry.Uid));
+ }
+ else if (referenceField is Entry singleRef)
+ {
+ TestAssert.NotNull(singleRef.Uid);
+ }
+ else
+ {
+ TestAssert.Fail($"Reference field '{referenceFieldName}' is not of expected type (Entry or List)");
+ }
+ }
+
+ ///
+ /// Asserts that a reference chain is populated to the specified depth
+ ///
+ public static void AssertReferenceChainDepth(Entry entry, string[] referenceFieldPath)
+ {
+ TestAssert.NotNull(entry);
+ TestAssert.NotEmpty(referenceFieldPath);
+
+ object current = entry;
+
+ foreach (var fieldName in referenceFieldPath)
+ {
+ if (current is Entry currentEntry)
+ {
+ var field = currentEntry.Get(fieldName);
+ TestAssert.NotNull(field);
+ current = field;
+ }
+ else if (current is List entryList)
+ {
+ TestAssert.NotEmpty(entryList);
+ current = entryList.First();
+ var field = ((Entry)current).Get(fieldName);
+ TestAssert.NotNull(field);
+ current = field;
+ }
+ else
+ {
+ TestAssert.Fail($"Unexpected type in reference chain: {current.GetType().Name}");
+ }
+ }
+ }
+
+ #endregion
+
+ #region Asset Assertions
+
+ ///
+ /// Asserts that an asset has all required fields populated
+ ///
+ public static void AssertAssetValid(Asset asset, string expectedUid = null)
+ {
+ TestAssert.NotNull(asset);
+ TestAssert.NotNull(asset.Uid);
+ TestAssert.NotEmpty(asset.Uid);
+ TestAssert.NotNull(asset.Url);
+ TestAssert.NotEmpty(asset.Url);
+ TestAssert.NotNull(asset.FileName);
+ TestAssert.NotEmpty(asset.FileName);
+
+ if (!string.IsNullOrEmpty(expectedUid))
+ {
+ TestAssert.Equal(expectedUid, asset.Uid);
+ }
+ }
+
+ ///
+ /// Asserts that a collection of assets is valid
+ ///
+ public static void AssertAssetsValid(IEnumerable assets, int? expectedMinCount = null)
+ {
+ TestAssert.NotNull(assets);
+
+ var assetsList = assets.ToList();
+ TestAssert.NotEmpty(assetsList);
+
+ if (expectedMinCount.HasValue)
+ {
+ TestAssert.True(assetsList.Count >= expectedMinCount.Value,
+ $"Expected at least {expectedMinCount.Value} assets, but got {assetsList.Count}");
+ }
+
+ TestAssert.All(assetsList, asset => AssertAssetValid(asset));
+ }
+
+ #endregion
+
+ #region Query Result Assertions
+
+ ///
+ /// Asserts that a ContentstackCollection result is valid
+ ///
+ public static void AssertQueryResultValid(ContentstackCollection result, int? expectedMinCount = null) where T : Entry
+ {
+ TestAssert.NotNull(result);
+ TestAssert.NotNull(result.Items);
+
+ var items = result.Items.ToList();
+
+ if (expectedMinCount.HasValue)
+ {
+ TestAssert.True(items.Count >= expectedMinCount.Value,
+ $"Expected at least {expectedMinCount.Value} items, but got {items.Count}");
+ }
+ }
+
+ ///
+ /// Asserts that query results are sorted correctly
+ ///
+ public static void AssertSortedAscending(IEnumerable items, Func keySelector) where TKey : IComparable
+ {
+ var itemsList = items.ToList();
+ TestAssert.True(itemsList.Count >= 2, "Need at least 2 items to verify sorting");
+
+ for (int i = 0; i < itemsList.Count - 1; i++)
+ {
+ var current = keySelector(itemsList[i]);
+ var next = keySelector(itemsList[i + 1]);
+
+ TestAssert.True(current.CompareTo(next) <= 0,
+ $"Items are not sorted ascending at index {i}. Current: {current}, Next: {next}");
+ }
+ }
+
+ ///
+ /// Asserts that query results are sorted descending
+ ///
+ public static void AssertSortedDescending(IEnumerable items, Func keySelector) where TKey : IComparable
+ {
+ var itemsList = items.ToList();
+ TestAssert.True(itemsList.Count >= 2, "Need at least 2 items to verify sorting");
+
+ for (int i = 0; i < itemsList.Count - 1; i++)
+ {
+ var current = keySelector(itemsList[i]);
+ var next = keySelector(itemsList[i + 1]);
+
+ TestAssert.True(current.CompareTo(next) >= 0,
+ $"Items are not sorted descending at index {i}. Current: {current}, Next: {next}");
+ }
+ }
+
+ #endregion
+
+ #region Field Projection Assertions
+
+ ///
+ /// Asserts that only specified fields are present
+ ///
+ public static void AssertOnlyFieldsPresent(Entry entry, string[] expectedFields)
+ {
+ TestAssert.NotNull(entry);
+ TestAssert.NotNull(expectedFields);
+
+ // UID is always present
+ var allowedFields = new List(expectedFields) { "uid" };
+
+ foreach (var key in entry.Object.Keys)
+ {
+ // Skip internal fields that start with underscore
+ if (key.StartsWith("_"))
+ continue;
+
+ TestAssert.Contains(key, allowedFields);
+ }
+ }
+
+ ///
+ /// Asserts that specified fields are excluded
+ ///
+ public static void AssertFieldsExcluded(Entry entry, string[] excludedFields)
+ {
+ TestAssert.NotNull(entry);
+ TestAssert.NotNull(excludedFields);
+
+ foreach (var field in excludedFields)
+ {
+ TestAssert.Null(entry.Get(field));
+ }
+ }
+
+ #endregion
+
+ #region Date/Time Assertions
+
+ ///
+ /// Asserts that a date string is valid and parseable
+ ///
+ public static void AssertValidDate(string dateString)
+ {
+ TestAssert.NotNull(dateString);
+ TestAssert.NotEmpty(dateString);
+ TestAssert.True(DateTime.TryParse(dateString, out _),
+ $"'{dateString}' is not a valid date");
+ }
+
+ ///
+ /// Asserts that a date is within an expected range
+ ///
+ public static void AssertDateInRange(DateTime date, DateTime minDate, DateTime maxDate)
+ {
+ TestAssert.True(date >= minDate && date <= maxDate,
+ $"Date {date} is not between {minDate} and {maxDate}");
+ }
+
+ #endregion
+
+ #region Error Assertions
+
+ ///
+ /// Asserts that an exception is thrown with a specific error code
+ ///
+ public static void AssertContentstackException(Action action, int? expectedErrorCode = null)
+ {
+ var exception = TestAssert.Throws(action);
+
+ if (expectedErrorCode.HasValue)
+ {
+ TestAssert.Equal(expectedErrorCode.Value, exception.ErrorCode);
+ }
+ }
+
+ ///
+ /// Asserts that an async exception is thrown with a specific error code
+ ///
+ public static async System.Threading.Tasks.Task AssertContentstackExceptionAsync(
+ Func action,
+ int? expectedErrorCode = null)
+ {
+ var exception = await TestAssert.ThrowsAsync(action);
+
+ if (expectedErrorCode.HasValue)
+ {
+ TestAssert.Equal(expectedErrorCode.Value, exception.ErrorCode);
+ }
+ }
+
+ #endregion
+
+ #region Asset Assertions
+
+ ///
+ /// Asserts that an asset has all basic required fields populated
+ ///
+ public static void AssertAssetBasicFields(Asset asset, string expectedUid = null)
+ {
+ TestAssert.NotNull(asset);
+ TestAssert.NotNull(asset.Uid);
+ TestAssert.NotEmpty(asset.Uid);
+
+ if (!string.IsNullOrEmpty(expectedUid))
+ {
+ TestAssert.Equal(expectedUid, asset.Uid);
+ }
+
+ // Required fields for assets
+ TestAssert.NotNull(asset.Url);
+ TestAssert.NotEmpty(asset.Url);
+ TestAssert.NotNull(asset.FileName);
+ TestAssert.NotEmpty(asset.FileName);
+ }
+
+ ///
+ /// Asserts that an asset URL is valid and accessible
+ ///
+ public static void AssertAssetUrl(Asset asset)
+ {
+ TestAssert.NotNull(asset);
+ TestAssert.NotNull(asset.Url);
+ TestAssert.NotEmpty(asset.Url);
+
+ // Verify it's a valid URL
+ TestAssert.True(Uri.TryCreate(asset.Url, UriKind.Absolute, out var uri),
+ $"Asset URL should be a valid absolute URL: {asset.Url}");
+ TestAssert.True(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps,
+ $"Asset URL should use HTTP or HTTPS: {asset.Url}");
+ }
+
+ #endregion
+
+ #region Stack/Client Assertions
+
+ ///
+ /// Asserts that a ContentstackClient is properly configured with given options
+ ///
+ public static void AssertStackConfiguration(
+ ContentstackClient client,
+ Configuration.ContentstackOptions options)
+ {
+ TestAssert.NotNull(client);
+ TestAssert.NotNull(options);
+
+ // Verify core configuration
+ TestAssert.Equal(options.ApiKey, client.GetApplicationKey());
+ TestAssert.Equal(options.DeliveryToken, client.GetAccessToken());
+
+ // Version should always be available
+ var version = client.GetVersion();
+ TestAssert.NotNull(version);
+ TestAssert.NotEmpty(version);
+ }
+
+ #endregion
+ }
+}
+
diff --git a/Contentstack.Core.Tests/Helpers/EntryFactory.cs b/Contentstack.Core.Tests/Helpers/EntryFactory.cs
new file mode 100644
index 0000000..33e9d81
--- /dev/null
+++ b/Contentstack.Core.Tests/Helpers/EntryFactory.cs
@@ -0,0 +1,239 @@
+using System;
+using System.Threading.Tasks;
+using Contentstack.Core.Models;
+
+namespace Contentstack.Core.Tests.Helpers
+{
+ ///
+ /// Factory class for creating and fetching entries in tests
+ /// Provides common patterns for entry retrieval
+ ///
+ public class EntryFactory
+ {
+ private readonly ContentstackClient _client;
+
+ ///
+ /// Initializes a new instance of EntryFactory
+ ///
+ /// Contentstack client instance
+ public EntryFactory(ContentstackClient client)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+
+ #region Single Entry Methods
+
+ ///
+ /// Fetches a single entry by UID
+ ///
+ /// Entry model type
+ /// Content type UID
+ /// Entry UID
+ /// Fetched entry
+ public async Task FetchEntryAsync(string contentTypeUid, string entryUid) where T : Entry
+ {
+ return await _client
+ .ContentType(contentTypeUid)
+ .Entry(entryUid)
+ .Fetch();
+ }
+
+ ///
+ /// Fetches a single entry with references
+ ///
+ /// Entry model type
+ /// Content type UID
+ /// Entry UID
+ /// Reference field UIDs to include
+ /// Fetched entry with references
+ public async Task FetchEntryWithReferencesAsync(
+ string contentTypeUid,
+ string entryUid,
+ params string[] referenceFields) where T : Entry
+ {
+ var entry = _client
+ .ContentType(contentTypeUid)
+ .Entry(entryUid);
+
+ foreach (var refField in referenceFields)
+ {
+ entry.IncludeReference(refField);
+ }
+
+ return await entry.Fetch();
+ }
+
+ ///
+ /// Fetches a single entry with all options
+ ///
+ /// Entry model type
+ /// Content type UID
+ /// Entry UID
+ /// Include metadata
+ /// Include branch
+ /// Include owner
+ /// Locale code
+ /// Include fallback locale
+ /// Fetched entry
+ public async Task FetchEntryWithOptionsAsync(
+ string contentTypeUid,
+ string entryUid,
+ bool includeMetadata = false,
+ bool includeBranch = false,
+ bool includeOwner = false,
+ string locale = null,
+ bool includeFallback = false) where T : Entry
+ {
+ var entry = _client
+ .ContentType(contentTypeUid)
+ .Entry(entryUid);
+
+ if (includeMetadata)
+ entry.IncludeMetadata();
+
+ if (includeBranch)
+ entry.IncludeBranch();
+
+ if (includeOwner)
+ entry.IncludeOwner();
+
+ if (!string.IsNullOrEmpty(locale))
+ {
+ entry.SetLocale(locale);
+
+ if (includeFallback)
+ entry.IncludeFallback();
+ }
+
+ return await entry.Fetch();
+ }
+
+ #endregion
+
+ #region Query Methods
+
+ ///
+ /// Creates a basic query for a content type
+ ///
+ /// Content type UID
+ /// Query instance
+ public Query CreateQuery(string contentTypeUid)
+ {
+ return _client.ContentType(contentTypeUid).Query();
+ }
+
+ ///
+ /// Fetches all entries for a content type
+ ///
+ /// Entry model type
+ /// Content type UID
+ /// Optional limit
+ /// Collection of entries
+ public async Task> FetchAllEntriesAsync(
+ string contentTypeUid,
+ int? limit = null) where T : Entry
+ {
+ var query = CreateQuery(contentTypeUid);
+
+ if (limit.HasValue)
+ query.Limit(limit.Value);
+
+ return await query.Find();
+ }
+
+ ///
+ /// Fetches entries with pagination
+ ///
+ /// Entry model type
+ /// Content type UID
+ /// Number to skip
+ /// Number to return
+ /// Collection of entries
+ public async Task> FetchEntriesWithPaginationAsync(
+ string contentTypeUid,
+ int skip,
+ int limit) where T : Entry
+ {
+ return await CreateQuery(contentTypeUid)
+ .Skip(skip)
+ .Limit(limit)
+ .Find();
+ }
+
+ ///
+ /// Fetches entries matching a field value
+ ///
+ /// Entry model type
+ /// Content type UID
+ /// Field name to match
+ /// Field value to match
+ /// Collection of matching entries
+ public async Task> FetchEntriesWhereAsync(
+ string contentTypeUid,
+ string fieldName,
+ object fieldValue) where T : Entry
+ {
+ return await CreateQuery(contentTypeUid)
+ .Where(fieldName, fieldValue)
+ .Find();
+ }
+
+ #endregion
+
+ #region Asset Methods
+
+ ///
+ /// Fetches a single asset by UID
+ ///
+ /// Asset UID
+ /// Fetched asset
+ public async Task FetchAssetAsync(string assetUid)
+ {
+ return await _client.Asset(assetUid).Fetch();
+ }
+
+ ///
+ /// Fetches all assets
+ ///
+ /// Optional limit
+ /// Collection of assets
+ public async Task> FetchAllAssetsAsync(int? limit = null)
+ {
+ var assetLibrary = _client.AssetLibrary();
+
+ if (limit.HasValue)
+ assetLibrary.Limit(limit.Value);
+
+ return await assetLibrary.FetchAll();
+ }
+
+ #endregion
+
+ #region Utility Methods
+
+ ///
+ /// Fetches the first entry from a query (convenience method)
+ /// FindOne returns a ContentstackCollection with limit=1
+ ///
+ /// Entry model type
+ /// Content type UID
+ /// ContentstackCollection with one entry
+ public async Task> FetchFirstEntryAsync(string contentTypeUid)
+ {
+ return await CreateQuery(contentTypeUid).FindOne();
+ }
+
+ ///
+ /// Counts entries in a content type
+ ///
+ /// Content type UID
+ /// Count result
+ public async Task CountEntriesAsync(string contentTypeUid)
+ {
+ return await CreateQuery(contentTypeUid).Count();
+ }
+
+ #endregion
+ }
+}
+
diff --git a/Contentstack.Core.Tests/Helpers/IntegrationTestBase.cs b/Contentstack.Core.Tests/Helpers/IntegrationTestBase.cs
new file mode 100644
index 0000000..050de61
--- /dev/null
+++ b/Contentstack.Core.Tests/Helpers/IntegrationTestBase.cs
@@ -0,0 +1,103 @@
+using System.Collections.Generic;
+using Xunit.Abstractions;
+using Contentstack.Core.Configuration;
+
+namespace Contentstack.Core.Tests.Helpers
+{
+ ///
+ /// Base class for integration tests with built-in enhanced logging support
+ /// Provides TestOutputHelper and common helper methods for logging
+ ///
+ public abstract class IntegrationTestBase
+ {
+ protected readonly ITestOutputHelper Output;
+ protected readonly TestOutputHelper TestOutput;
+
+ protected IntegrationTestBase(ITestOutputHelper output)
+ {
+ Output = output;
+ TestOutput = new TestOutputHelper(output, GetType().Name);
+ TestAssert.SetHelper(TestOutput);
+ }
+
+ ///
+ /// Log test arrangement step with context
+ ///
+ protected void LogArrange(string description, Dictionary context = null)
+ {
+ TestOutput.LogStep("Arrange", description);
+
+ if (context != null)
+ {
+ foreach (var kvp in context)
+ {
+ TestOutput.LogContext(kvp.Key, kvp.Value);
+ }
+ }
+ }
+
+ ///
+ /// Log test action step
+ ///
+ protected void LogAct(string description)
+ {
+ TestOutput.LogStep("Act", description);
+ }
+
+ ///
+ /// Log test assertion step
+ ///
+ protected void LogAssert(string description)
+ {
+ TestOutput.LogStep("Assert", description);
+ }
+
+ ///
+ /// Log assertion with expected and actual values
+ ///
+ protected void LogAssertion(string name, object expected, object actual)
+ {
+ var passed = AreEqual(expected, actual);
+ TestOutput.LogAssertion(name, expected, actual, passed);
+ }
+
+ ///
+ /// Log context information
+ ///
+ protected void LogContext(string key, object value)
+ {
+ TestOutput.LogContext(key, value);
+ }
+
+ ///
+ /// Helper to check equality for logging
+ ///
+ private bool AreEqual(object expected, object actual)
+ {
+ if (expected == null && actual == null) return true;
+ if (expected == null || actual == null) return false;
+ return expected.Equals(actual) || expected.ToString() == actual.ToString();
+ }
+
+ ///
+ /// Create Contentstack client with standard configuration.
+ /// Automatically registers RequestLoggingPlugin to capture actual HTTP requests/responses.
+ ///
+ protected ContentstackClient CreateClient()
+ {
+ var options = new ContentstackOptions()
+ {
+ Host = TestDataHelper.Host,
+ ApiKey = TestDataHelper.ApiKey,
+ DeliveryToken = TestDataHelper.DeliveryToken,
+ Environment = TestDataHelper.Environment,
+ Branch = TestDataHelper.BranchUid
+ };
+
+ var client = new ContentstackClient(options);
+ client.Plugins.Add(new RequestLoggingPlugin(TestOutput));
+ return client;
+ }
+
+ }
+}
diff --git a/Contentstack.Core.Tests/Helpers/PerformanceHelper.cs b/Contentstack.Core.Tests/Helpers/PerformanceHelper.cs
new file mode 100644
index 0000000..cd502a9
--- /dev/null
+++ b/Contentstack.Core.Tests/Helpers/PerformanceHelper.cs
@@ -0,0 +1,241 @@
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Contentstack.Core.Tests.Helpers
+{
+ ///
+ /// Helper class for performance measurement and benchmarking
+ ///
+ public static class PerformanceHelper
+ {
+ #region Performance Thresholds
+
+ ///
+ /// Default timeout for single entry fetch (5 seconds)
+ ///
+ public const int DefaultSingleFetchThresholdMs = 5000;
+
+ ///
+ /// Default timeout for query operations (10 seconds)
+ ///
+ public const int DefaultQueryThresholdMs = 10000;
+
+ ///
+ /// Default timeout for deep reference queries (15 seconds)
+ ///
+ public const int DefaultDeepReferenceThresholdMs = 15000;
+
+ ///
+ /// Default timeout for sync operations (30 seconds)
+ ///
+ public const int DefaultSyncThresholdMs = 30000;
+
+ #endregion
+
+ #region Measurement Methods
+
+ ///
+ /// Measures the execution time of a synchronous action
+ ///
+ /// Action to measure
+ /// Elapsed milliseconds
+ public static long MeasureExecutionTime(Action action)
+ {
+ var stopwatch = Stopwatch.StartNew();
+ action();
+ stopwatch.Stop();
+ return stopwatch.ElapsedMilliseconds;
+ }
+
+ ///
+ /// Measures the execution time of an asynchronous action
+ ///
+ /// Async action to measure
+ /// Elapsed milliseconds
+ public static async Task MeasureExecutionTimeAsync(Func action)
+ {
+ var stopwatch = Stopwatch.StartNew();
+ await action();
+ stopwatch.Stop();
+ return stopwatch.ElapsedMilliseconds;
+ }
+
+ ///
+ /// Measures the execution time and returns both result and time
+ ///
+ /// Return type
+ /// Function to measure
+ /// Tuple of (result, elapsed milliseconds)
+ public static async Task<(T result, long elapsedMs)> MeasureExecutionTimeAsync(Func> func)
+ {
+ var stopwatch = Stopwatch.StartNew();
+ var result = await func();
+ stopwatch.Stop();
+ return (result, stopwatch.ElapsedMilliseconds);
+ }
+
+ #endregion
+
+ #region Assertion Methods
+
+ ///
+ /// Asserts that an operation completes within the specified threshold
+ ///
+ /// Action to execute
+ /// Threshold in milliseconds
+ /// Name of the operation for error messages
+ public static void AssertPerformance(Action action, int thresholdMs, string operationName = "Operation")
+ {
+ var elapsed = MeasureExecutionTime(action);
+ Assert.True(elapsed < thresholdMs,
+ $"{operationName} took {elapsed}ms, expected < {thresholdMs}ms (threshold exceeded by {elapsed - thresholdMs}ms)");
+ }
+
+ ///
+ /// Asserts that an async operation completes within the specified threshold
+ ///
+ /// Async action to execute
+ /// Threshold in milliseconds
+ /// Name of the operation for error messages
+ public static async Task AssertPerformanceAsync(Func action, int thresholdMs, string operationName = "Operation")
+ {
+ var elapsed = await MeasureExecutionTimeAsync(action);
+ Assert.True(elapsed < thresholdMs,
+ $"{operationName} took {elapsed}ms, expected < {thresholdMs}ms (threshold exceeded by {elapsed - thresholdMs}ms)");
+ }
+
+ ///
+ /// Asserts that an async operation with result completes within the specified threshold
+ ///
+ /// Return type
+ /// Async function to execute
+ /// Threshold in milliseconds
+ /// Name of the operation for error messages
+ /// The result from the function
+ public static async Task AssertPerformanceAsync(Func> func, int thresholdMs, string operationName = "Operation")
+ {
+ var (result, elapsed) = await MeasureExecutionTimeAsync(func);
+ Assert.True(elapsed < thresholdMs,
+ $"{operationName} took {elapsed}ms, expected < {thresholdMs}ms (threshold exceeded by {elapsed - thresholdMs}ms)");
+ return result;
+ }
+
+ #endregion
+
+ #region Benchmarking Methods
+
+ ///
+ /// Runs a benchmark of an operation multiple times and returns statistics
+ ///
+ /// Action to benchmark
+ /// Number of iterations to run
+ /// Benchmark statistics
+ public static BenchmarkResult Benchmark(Action action, int iterations = 10)
+ {
+ var times = new long[iterations];
+
+ for (int i = 0; i < iterations; i++)
+ {
+ times[i] = MeasureExecutionTime(action);
+ }
+
+ return new BenchmarkResult(times);
+ }
+
+ ///
+ /// Runs an async benchmark of an operation multiple times and returns statistics
+ ///
+ /// Async action to benchmark
+ /// Number of iterations to run
+ /// Benchmark statistics
+ public static async Task BenchmarkAsync(Func action, int iterations = 10)
+ {
+ var times = new long[iterations];
+
+ for (int i = 0; i < iterations; i++)
+ {
+ times[i] = await MeasureExecutionTimeAsync(action);
+ }
+
+ return new BenchmarkResult(times);
+ }
+
+ #endregion
+
+ #region Benchmark Result Class
+
+ ///
+ /// Contains statistics from a benchmark run
+ ///
+ public class BenchmarkResult
+ {
+ public long[] AllTimes { get; }
+ public long MinMs { get; }
+ public long MaxMs { get; }
+ public long AverageMs { get; }
+ public long MedianMs { get; }
+ public int Iterations { get; }
+
+ public BenchmarkResult(long[] times)
+ {
+ AllTimes = times;
+ Iterations = times.Length;
+
+ if (times.Length == 0)
+ {
+ MinMs = MaxMs = AverageMs = MedianMs = 0;
+ return;
+ }
+
+ MinMs = long.MaxValue;
+ MaxMs = long.MinValue;
+ long sum = 0;
+
+ foreach (var time in times)
+ {
+ if (time < MinMs) MinMs = time;
+ if (time > MaxMs) MaxMs = time;
+ sum += time;
+ }
+
+ AverageMs = sum / times.Length;
+
+ // Calculate median
+ Array.Sort(times);
+ MedianMs = times[times.Length / 2];
+ }
+
+ public override string ToString()
+ {
+ return $"Benchmark Results ({Iterations} iterations):\n" +
+ $" Min: {MinMs}ms\n" +
+ $" Max: {MaxMs}ms\n" +
+ $" Avg: {AverageMs}ms\n" +
+ $" Median: {MedianMs}ms";
+ }
+
+ ///
+ /// Asserts that the average time is within threshold
+ ///
+ public void AssertAverageWithinThreshold(int thresholdMs, string operationName = "Operation")
+ {
+ Assert.True(AverageMs < thresholdMs,
+ $"{operationName} average time {AverageMs}ms exceeded threshold {thresholdMs}ms\n{this}");
+ }
+
+ ///
+ /// Asserts that the max time is within threshold
+ ///
+ public void AssertMaxWithinThreshold(int thresholdMs, string operationName = "Operation")
+ {
+ Assert.True(MaxMs < thresholdMs,
+ $"{operationName} max time {MaxMs}ms exceeded threshold {thresholdMs}ms\n{this}");
+ }
+ }
+
+ #endregion
+ }
+}
+
diff --git a/Contentstack.Core.Tests/Helpers/RequestLoggingPlugin.cs b/Contentstack.Core.Tests/Helpers/RequestLoggingPlugin.cs
new file mode 100644
index 0000000..23b464a
--- /dev/null
+++ b/Contentstack.Core.Tests/Helpers/RequestLoggingPlugin.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Contentstack.Core.Interfaces;
+
+namespace Contentstack.Core.Tests.Helpers
+{
+ ///
+ /// SDK Plugin that captures the ACTUAL HTTP request and response made by the SDK.
+ /// Implements IContentstackPlugin to intercept requests via the SDK's plugin pipeline.
+ /// This gives us the real URL (with all query params like environment, locale,
+ /// include_fallback, query filters, etc.) and real headers (api_key, access_token,
+ /// branch, x-cs-variant-uid, etc.) — not a manual approximation.
+ /// Also detects and logs the SDK method chain from URL patterns.
+ ///
+ public class RequestLoggingPlugin : IContentstackPlugin
+ {
+ private readonly TestOutputHelper _output;
+
+ public RequestLoggingPlugin(TestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ public async Task OnRequest(ContentstackClient stack, HttpWebRequest request)
+ {
+ if (_output == null)
+ return request;
+
+ // Capture ALL headers from the actual request
+ var headers = new Dictionary();
+
+ // Standard headers set directly on HttpWebRequest
+ if (!string.IsNullOrEmpty(request.ContentType))
+ headers["Content-Type"] = request.ContentType;
+
+ // All custom headers (api_key, access_token, branch, x-cs-variant-uid, x-user-agent, etc.)
+ foreach (string key in request.Headers.AllKeys)
+ {
+ headers[key] = request.Headers[key];
+ }
+
+ // Detect SDK method from URL pattern
+ var sdkMethod = DetectSdkMethod(request.Method, request.RequestUri.ToString(), headers);
+
+ // Log the REAL request with REAL URL (includes all SDK-added query params)
+ _output.LogRequest(
+ request.Method,
+ request.RequestUri.ToString(),
+ headers,
+ null,
+ sdkMethod
+ );
+
+ return await Task.FromResult(request);
+ }
+
+ public async Task OnResponse(ContentstackClient stack, HttpWebRequest request, HttpWebResponse response, string responseString)
+ {
+ if (_output == null)
+ return responseString;
+
+ // Capture response headers
+ var headers = new Dictionary();
+ foreach (string key in response.Headers.AllKeys)
+ {
+ headers[key] = response.Headers[key];
+ }
+
+ // Truncate response body for logging (keep first 2000 chars)
+ var truncatedBody = responseString;
+ if (!string.IsNullOrEmpty(responseString) && responseString.Length > 2000)
+ {
+ truncatedBody = responseString.Substring(0, 2000) + "... [truncated]";
+ }
+
+ _output.LogResponse(
+ (int)response.StatusCode,
+ response.StatusDescription,
+ headers,
+ truncatedBody
+ );
+
+ return await Task.FromResult(responseString);
+ }
+
+ // ====================================================================
+ // SDK METHOD DETECTION
+ // Maps HTTP method + URL pattern to .NET CDA SDK method chains
+ // ====================================================================
+
+ private static readonly (Regex pattern, string method, string sdk)[] SdkMethodPatterns = new[]
+ {
+ // Sync API
+ (new Regex(@"/v3/stacks/sync\b"), "GET", "client.SyncRecursive() / SyncToken() / SyncPaginationToken()"),
+
+ // Content Types
+ (new Regex(@"/v3/content_types/[^/]+/entries/[^/?]+"), "GET", "client.ContentType(uid).Entry(uid).Fetch()"),
+ (new Regex(@"/v3/content_types/[^/]+/entries\b"), "GET", "client.ContentType(uid).Query().Find()"),
+ (new Regex(@"/v3/content_types/[^/?]+$"), "GET", "client.ContentType(uid).Fetch()"),
+ (new Regex(@"/v3/content_types\b"), "GET", "client.GetContentTypes()"),
+
+ // Assets
+ (new Regex(@"/v3/assets/[^/?]+$"), "GET", "client.Asset(uid).Fetch()"),
+ (new Regex(@"/v3/assets\b"), "GET", "client.AssetLibrary().FetchAll()"),
+
+ // Global Fields
+ (new Regex(@"/v3/global_fields/[^/?]+$"), "GET", "client.GlobalField(uid).Fetch()"),
+ (new Regex(@"/v3/global_fields\b"), "GET", "client.GlobalFieldQuery().Find()"),
+
+ // Taxonomies
+ (new Regex(@"/v3/taxonomies/[^/]+/terms\b"), "GET", "taxonomy.Terms().Query()"),
+ (new Regex(@"/v3/taxonomies/entries\b"), "GET", "client.Taxonomies().Entries()"),
+ (new Regex(@"/v3/taxonomies/[^/?]+$"), "GET", "client.Taxonomy(uid).Fetch()"),
+ (new Regex(@"/v3/taxonomies\b"), "GET", "client.Taxonomies().Query()"),
+
+ // Image Delivery (image transform URLs)
+ (new Regex(@"/v3/assets/.*\?.*(?:width|height|format|quality|crop|trim|orient)"), "GET", "client.Asset(uid).Fetch() [with ImageTransform]"),
+
+ // Live Preview
+ (new Regex(@"/v3/content_types.*live_preview"), "GET", "client.LivePreviewQueryAsync()"),
+ };
+
+ ///
+ /// Detects the SDK method chain from the HTTP request URL pattern.
+ /// Also checks headers for additional context (variants, branch).
+ ///
+ private static string DetectSdkMethod(string httpMethod, string url, Dictionary headers)
+ {
+ if (string.IsNullOrEmpty(url))
+ return null;
+
+ var method = httpMethod?.ToUpper() ?? "GET";
+
+ // Extract path from URL (remove query string for pattern matching)
+ string path;
+ try
+ {
+ var uri = new Uri(url);
+ path = uri.AbsolutePath;
+ }
+ catch
+ {
+ path = url;
+ }
+
+ // Find matching pattern
+ string sdkMethod = null;
+ foreach (var mapping in SdkMethodPatterns)
+ {
+ if (mapping.method == method && mapping.pattern.IsMatch(path))
+ {
+ sdkMethod = mapping.sdk;
+ break;
+ }
+ }
+
+ if (sdkMethod == null)
+ return $"Unknown ({method} {path})";
+
+ // Enrich with header context
+ var extras = new List();
+
+ if (headers.ContainsKey("x-cs-variant-uid"))
+ extras.Add(".Variant()");
+
+ if (headers.ContainsKey("branch"))
+ extras.Add($"[branch: {headers["branch"]}]");
+
+ if (extras.Count > 0)
+ sdkMethod += " " + string.Join(" ", extras);
+
+ return sdkMethod;
+ }
+ }
+}
diff --git a/Contentstack.Core.Tests/Helpers/TestAssert.cs b/Contentstack.Core.Tests/Helpers/TestAssert.cs
new file mode 100644
index 0000000..d22a1c5
--- /dev/null
+++ b/Contentstack.Core.Tests/Helpers/TestAssert.cs
@@ -0,0 +1,352 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Contentstack.Core.Tests.Helpers
+{
+ ///
+ /// Drop-in replacement for xUnit Assert that automatically logs Expected vs Actual values.
+ /// Uses CallerArgumentExpression (C# 10+) to capture the expression being asserted,
+ /// and AsyncLocal to route logs to the current test's TestOutputHelper.
+ ///
+ /// Usage: Replace 'Assert.' with 'TestAssert.' in test files.
+ /// The IntegrationTestBase constructor calls TestAssert.SetHelper(TestOutput) automatically.
+ ///
+ public static class TestAssert
+ {
+ private static readonly AsyncLocal _helper = new();
+
+ ///
+ /// Set the TestOutputHelper for the current async context (called by IntegrationTestBase ctor)
+ ///
+ public static void SetHelper(TestOutputHelper helper)
+ {
+ _helper.Value = helper;
+ }
+
+ private static void Log(string name, object expected, object actual, bool passed)
+ {
+ _helper.Value?.LogAssertion(name ?? "assertion", expected, actual, passed);
+ }
+
+ #region NotNull / Null
+
+ public static void NotNull(
+ object obj,
+ [CallerArgumentExpression(nameof(obj))] string expr = null)
+ {
+ Log(expr, "NotNull", obj != null ? Truncate(obj) : "null", obj != null);
+ Assert.NotNull(obj);
+ }
+
+ public static void Null(
+ object obj,
+ [CallerArgumentExpression(nameof(obj))] string expr = null)
+ {
+ Log(expr, "null", obj == null ? "null" : Truncate(obj), obj == null);
+ Assert.Null(obj);
+ }
+
+ #endregion
+
+ #region Equal / NotEqual
+
+ public static void Equal(
+ T expected, T actual,
+ [CallerArgumentExpression(nameof(actual))] string expr = null)
+ {
+ bool passed = EqualityComparer.Default.Equals(expected, actual);
+ Log(expr, Truncate(expected), Truncate(actual), passed);
+ Assert.Equal(expected, actual);
+ }
+
+ public static void NotEqual(
+ T expected, T actual,
+ [CallerArgumentExpression(nameof(actual))] string expr = null)
+ {
+ bool passed = !EqualityComparer.Default.Equals(expected, actual);
+ Log(expr, $"Not: {Truncate(expected)}", Truncate(actual), passed);
+ Assert.NotEqual(expected, actual);
+ }
+
+ #endregion
+
+ #region NotEmpty / Empty
+
+ public static void NotEmpty(
+ IEnumerable collection,
+ [CallerArgumentExpression(nameof(collection))] string expr = null)
+ {
+ string display;
+ bool hasItems;
+
+ if (collection is string s)
+ {
+ hasItems = !string.IsNullOrEmpty(s);
+ display = hasItems ? $"\"{Truncate(s, 80)}\"" : "(empty string)";
+ }
+ else if (collection != null)
+ {
+ var enumerator = collection.GetEnumerator();
+ hasItems = enumerator.MoveNext();
+ if (enumerator is IDisposable d) d.Dispose();
+ display = hasItems ? "(has items)" : "(empty collection)";
+ }
+ else
+ {
+ hasItems = false;
+ display = "null";
+ }
+
+ Log(expr, "NotEmpty", display, hasItems);
+ Assert.NotEmpty(collection);
+ }
+
+ public static void Empty(
+ IEnumerable collection,
+ [CallerArgumentExpression(nameof(collection))] string expr = null)
+ {
+ string display;
+ bool isEmpty;
+
+ if (collection is string s)
+ {
+ isEmpty = string.IsNullOrEmpty(s);
+ display = isEmpty ? "(empty string)" : $"\"{Truncate(s, 80)}\"";
+ }
+ else if (collection != null)
+ {
+ var enumerator = collection.GetEnumerator();
+ isEmpty = !enumerator.MoveNext();
+ if (enumerator is IDisposable d) d.Dispose();
+ display = isEmpty ? "(empty collection)" : "(has items)";
+ }
+ else
+ {
+ isEmpty = true;
+ display = "null";
+ }
+
+ Log(expr, "Empty", display, isEmpty);
+ Assert.Empty(collection);
+ }
+
+ #endregion
+
+ #region True / False
+
+ public static void True(
+ bool condition,
+ string userMessage = null,
+ [CallerArgumentExpression(nameof(condition))] string expr = null)
+ {
+ Log(userMessage ?? expr, true, condition, condition);
+ if (userMessage != null)
+ Assert.True(condition, userMessage);
+ else
+ Assert.True(condition);
+ }
+
+ public static void False(
+ bool condition,
+ string userMessage = null,
+ [CallerArgumentExpression(nameof(condition))] string expr = null)
+ {
+ Log(userMessage ?? expr, false, condition, !condition);
+ if (userMessage != null)
+ Assert.False(condition, userMessage);
+ else
+ Assert.False(condition);
+ }
+
+ #endregion
+
+ #region Contains / DoesNotContain
+
+ public static void Contains(
+ string expectedSubstring, string actualString,
+ [CallerArgumentExpression(nameof(actualString))] string expr = null)
+ {
+ bool passed = actualString != null && actualString.Contains(expectedSubstring);
+ Log($"Contains in {expr}", $"\"{Truncate(expectedSubstring, 50)}\"",
+ passed ? $"Found in \"{Truncate(actualString, 80)}\"" : $"Not found in \"{Truncate(actualString, 80)}\"", passed);
+ Assert.Contains(expectedSubstring, actualString);
+ }
+
+ public static void Contains(
+ T expected, IEnumerable collection,
+ [CallerArgumentExpression(nameof(collection))] string expr = null)
+ {
+ bool passed = collection != null && collection.Contains(expected);
+ Log($"Contains in {expr}", Truncate(expected), passed ? "Found" : "Not found", passed);
+ Assert.Contains(expected, collection);
+ }
+
+ public static void DoesNotContain(
+ string expectedSubstring, string actualString,
+ [CallerArgumentExpression(nameof(actualString))] string expr = null)
+ {
+ bool passed = actualString == null || !actualString.Contains(expectedSubstring);
+ Log($"DoesNotContain in {expr}", $"Should not contain \"{Truncate(expectedSubstring, 50)}\"",
+ passed ? "Not found" : "Found", passed);
+ Assert.DoesNotContain(expectedSubstring, actualString);
+ }
+
+ public static void DoesNotContain(
+ T expected, IEnumerable collection,
+ [CallerArgumentExpression(nameof(collection))] string expr = null)
+ {
+ bool passed = collection == null || !collection.Contains(expected);
+ Log($"DoesNotContain in {expr}", $"Should not contain {Truncate(expected)}",
+ passed ? "Not found" : "Found", passed);
+ Assert.DoesNotContain(expected, collection);
+ }
+
+ public static void Contains(
+ IEnumerable collection, Predicate filter,
+ [CallerArgumentExpression(nameof(filter))] string expr = null)
+ {
+ Assert.Contains(collection, filter);
+ Log($"Contains (predicate): {expr}", "Match found", "Match found", true);
+ }
+
+ public static void DoesNotContain(
+ IEnumerable collection, Predicate filter,
+ [CallerArgumentExpression(nameof(filter))] string expr = null)
+ {
+ Assert.DoesNotContain(collection, filter);
+ Log($"DoesNotContain (predicate): {expr}", "No match", "No match", true);
+ }
+
+ #endregion
+
+ #region Matches / DoesNotMatch / StartsWith
+
+ public static void Matches(
+ string regexPattern, string actualString,
+ [CallerArgumentExpression(nameof(actualString))] string expr = null)
+ {
+ bool passed = actualString != null && Regex.IsMatch(actualString, regexPattern);
+ Log($"Matches: {expr}", $"Pattern: {regexPattern}", Truncate(actualString, 100), passed);
+ Assert.Matches(regexPattern, actualString);
+ }
+
+ public static void DoesNotMatch(
+ string regexPattern, string actualString,
+ [CallerArgumentExpression(nameof(actualString))] string expr = null)
+ {
+ bool passed = actualString == null || !Regex.IsMatch(actualString, regexPattern);
+ Log($"DoesNotMatch: {expr}", $"Not: {regexPattern}", Truncate(actualString, 100), passed);
+ Assert.DoesNotMatch(regexPattern, actualString);
+ }
+
+ public static void StartsWith(
+ string expectedStartString, string actualString,
+ [CallerArgumentExpression(nameof(actualString))] string expr = null)
+ {
+ bool passed = actualString != null && actualString.StartsWith(expectedStartString);
+ Log($"StartsWith: {expr}", $"\"{Truncate(expectedStartString, 50)}\"",
+ $"\"{Truncate(actualString, 80)}\"", passed);
+ Assert.StartsWith(expectedStartString, actualString);
+ }
+
+ #endregion
+
+ #region Type Assertions
+
+ public static T IsType(
+ object obj,
+ [CallerArgumentExpression(nameof(obj))] string expr = null)
+ {
+ bool passed = obj != null && obj.GetType() == typeof(T);
+ Log($"IsType: {expr}", typeof(T).Name, obj?.GetType()?.Name ?? "null", passed);
+ return Assert.IsType(obj);
+ }
+
+ public static T IsAssignableFrom(
+ object obj,
+ [CallerArgumentExpression(nameof(obj))] string expr = null)
+ {
+ bool passed = obj is T;
+ Log($"IsAssignableFrom: {expr}", typeof(T).Name, obj?.GetType()?.Name ?? "null", passed);
+ return Assert.IsAssignableFrom(obj);
+ }
+
+ #endregion
+
+ #region Collection Assertions
+
+ public static void All(IEnumerable collection, Action action)
+ {
+ Assert.All(collection, action);
+ }
+
+ public static T Single(
+ IEnumerable collection,
+ [CallerArgumentExpression(nameof(collection))] string expr = null)
+ {
+ var list = collection?.ToList();
+ int count = list?.Count ?? 0;
+ Log($"Single: {expr}", "1 item", $"{count} item(s)", count == 1);
+ return Assert.Single(list);
+ }
+
+ public static void InRange(
+ T actual, T low, T high,
+ [CallerArgumentExpression(nameof(actual))] string expr = null) where T : IComparable, IComparable
+ {
+ bool passed = actual.CompareTo(low) >= 0 && actual.CompareTo(high) <= 0;
+ Log($"InRange: {expr}", $"[{low} .. {high}]", Truncate(actual), passed);
+ Assert.InRange(actual, low, high);
+ }
+
+ #endregion
+
+ #region Exception Assertions (pass-through, logging is less useful for lambdas)
+
+ public static T Throws(Action action) where T : Exception
+ => Assert.Throws(action);
+
+ public static T Throws(Func