From a3b1968ef8751fc41278c2ab5702abb439d8a6a9 Mon Sep 17 00:00:00 2001 From: Nadeem Patwekar Date: Tue, 7 Oct 2025 17:04:40 +0530 Subject: [PATCH 1/3] feat: Add query and where methods to AssetLibrary for enhanced query handling --- .talismanrc | 6 +- Contentstack.Core.Tests/AssetTest.cs | 290 +++++++++++++++++++++++ Contentstack.Core/Models/AssetLibrary.cs | 123 ++++++++-- 3 files changed, 390 insertions(+), 29 deletions(-) diff --git a/.talismanrc b/.talismanrc index 7654d1e..f993ff2 100644 --- a/.talismanrc +++ b/.talismanrc @@ -9,7 +9,9 @@ fileignoreconfig: - filename: Contentstack.Core/ContentstackClient.cs checksum: 687dc0a5f20037509731cfe540dcec9c3cc2b6cf50373cd183ece4f3249dc88e - filename: Contentstack.Core/Models/AssetLibrary.cs - checksum: 7e05fd0bbb43b15e6b7f387d746cc64d709e17e0e8e26a7495a99025077ff507 + checksum: 0c67f8bb3b7ffdb9b04cd38ae096904b53d6d4372e86c91c1f935e36b6b0ce56 +- filename: Contentstack.Core.Tests/AssetTest.cs + checksum: 9e197065aa6ea46af795a8ddb9d652a4972d9d4b4bfc7b1772d304d848f1c3e1 - filename: Contentstack.Core/Models/Asset.cs checksum: d192718723e6cb2aa8f08f873d3a7ea7268c89cc15da3bdeea4c16fd304c410e - filename: Contentstack.Core/Models/Query.cs @@ -20,7 +22,5 @@ fileignoreconfig: checksum: 53ba4ce874c4d2362ad00deb23f5a6ec219318860352f997b945e9161a580651 - filename: Contentstack.Core.Tests/ContentstackClientTest.cs checksum: b63897181a8cb5993d1305248cfc3e711c4039b5677b6c1e4e2a639e4ecb391b -- filename: Contentstack.Core.Tests/AssetTest.cs - checksum: 3e7bf50c7223c458561f0217484d5e70cf3770490c569e0a7083b0a12af9ab86 - filename: Contentstack.Core.Tests/RegionHandlerTest.cs checksum: 69899138754908e156aa477d775d12fd6b3fefc1a6c2afec22cb409bd6e6446c diff --git a/Contentstack.Core.Tests/AssetTest.cs b/Contentstack.Core.Tests/AssetTest.cs index 25f4fd7..e716901 100644 --- a/Contentstack.Core.Tests/AssetTest.cs +++ b/Contentstack.Core.Tests/AssetTest.cs @@ -661,5 +661,295 @@ public async Task AssetTags_CaseSensitivityVerification_ShouldTestCaseBehavior_T } } } + + [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/Models/AssetLibrary.cs b/Contentstack.Core/Models/AssetLibrary.cs index 8ec17e8..cc7643c 100644 --- a/Contentstack.Core/Models/AssetLibrary.cs +++ b/Contentstack.Core/Models/AssetLibrary.cs @@ -12,8 +12,6 @@ namespace Contentstack.Core.Models { public class AssetLibrary { - - #region Internal Variables private Dictionary _ObjectAttributes = new Dictionary(); private Dictionary _Headers = new Dictionary(); @@ -30,17 +28,12 @@ private string _Url } #endregion - public ContentstackClient Stack - { - get; - set; - } + public ContentstackClient Stack { get; set; } #region Internal Constructors - internal AssetLibrary() - { - } + internal AssetLibrary() { } + internal AssetLibrary(ContentstackClient stack) { this.Stack = stack; @@ -74,13 +67,12 @@ public void SortWithKeyAndOrderBy(String key, OrderBy order) UrlQueries.Add("desc", key); } } + /// /// Provides only the number of assets. /// /// /// - - /// ContentstackClient stack = new ContentstackClinet("api_key", "delivery_token", "environment"); /// AssetLibrary assetLibrary = stack.AssetLibrary(); /// JObject jObject = await assetLibrary.Count(); @@ -96,7 +88,30 @@ public AssetLibrary Query(JObject QueryObject) { try { - UrlQueries.Add("query", QueryObject); + if (UrlQueries.ContainsKey("query")) + { + // If query already exists, append/merge the new query object + JObject existingQuery = UrlQueries["query"] as JObject; + if (existingQuery != null) + { + // Merge the new query object with the existing one + existingQuery.Merge(QueryObject, new JsonMergeSettings + { + MergeArrayHandling = MergeArrayHandling.Union + }); + UrlQueries["query"] = existingQuery; + } + else + { + // If existing query is not a JObject, replace it + UrlQueries["query"] = QueryObject; + } + } + else + { + // If query doesn't exist, add it + UrlQueries.Add("query", QueryObject); + } } catch (Exception e) { @@ -104,6 +119,63 @@ public AssetLibrary Query(JObject QueryObject) } return this; } + + /// + /// Adds a key-value pair to the query object in UrlQueries. + /// + /// The key to add to the query object. + /// The value to add to the query object. + /// Current instance of AssetLibrary, this will be useful for a chaining calls. + /// + /// + /// ContentstackClient stack = new ContentstackClinet("api_key", "delivery_token", "environment"); + /// AssetLibrary assetLibrary = stack.AssetLibrary(); + /// assetLibrary.Where("filename", "image.png"); + /// ContentstackCollection contentstackCollection = await assetLibrary.FetchAll(); + /// + /// + public AssetLibrary Where(string key, string value) + { + try + { + // Handle null or empty key gracefully + if (string.IsNullOrEmpty(key)) + { + return this; + } + + if (UrlQueries.ContainsKey("query")) + { + // If query already exists, get it and add the key-value pair + JObject existingQuery = UrlQueries["query"] as JObject; + if (existingQuery != null) + { + existingQuery[key] = value; + UrlQueries["query"] = existingQuery; + } + else + { + // If existing query is not a JObject, create a new one + JObject newQuery = new JObject(); + newQuery[key] = value; + UrlQueries["query"] = newQuery; + } + } + else + { + // If query doesn't exist, create a new one with the key-value pair + JObject newQuery = new JObject(); + newQuery[key] = value; + UrlQueries.Add("query", newQuery); + } + } + catch (Exception e) + { + throw new Exception(StackConstants.ErrorMessage_QueryFilterException, e); + } + return this; + } + /// /// Include fallback locale publish content, if specified locale content is not publish. /// @@ -154,7 +226,6 @@ public AssetLibrary IncludeBranch() return this; } - /// /// Add param in URL query. /// @@ -167,7 +238,6 @@ public AssetLibrary IncludeBranch() /// ContentstackCollection contentstackCollection = await assetLibrary.FetchAll(); /// /// - /// Where function public AssetLibrary AddParam(string key, string value) { UrlQueries.Add(key, value); @@ -218,9 +288,6 @@ public void IncludeCount() UrlQueries.Add("include_count", "true"); } - - - /// /// This call includes metadata in the response. /// @@ -246,8 +313,6 @@ public AssetLibrary IncludeMetadata() return this; } - - /// /// This method includes the relative url of assets. /// @@ -465,7 +530,8 @@ public AssetLibrary RemoveHeader(string key) public async Task> FetchAll() { JObject json = await Exec(); - var assets = json.SelectToken("$.assets").ToObject>(this.Stack.Serializer); + var assets = json.SelectToken("$.assets") + .ToObject>(this.Stack.Serializer); var collection = json.ToObject>(this.Stack.Serializer); foreach (var entry in assets) { @@ -502,9 +568,15 @@ private async Task Exec() try { HttpRequestHandler RequestHandler = new HttpRequestHandler(this.Stack); - var outputResult = await RequestHandler.ProcessRequest(_Url, headers, mainJson, Branch: this.Stack.Config.Branch, timeout: this.Stack.Config.Timeout, proxy: this.Stack.Config.Proxy); + var outputResult = await RequestHandler.ProcessRequest( + _Url, + headers, + mainJson, + Branch: this.Stack.Config.Branch, + timeout: this.Stack.Config.Timeout, + proxy: this.Stack.Config.Proxy + ); return JObject.Parse(ContentstackConvert.ToString(outputResult, "{}")); - } catch (Exception ex) { @@ -540,19 +612,18 @@ private Dictionary GetHeader(Dictionary localHea } return classHeaders; - } else { return localHeader; } - } else { return _StackHeaders; } } + internal static ContentstackException GetContentstackError(Exception ex) { Int32 errorCode = 0; @@ -599,7 +670,7 @@ internal static ContentstackException GetContentstackError(Exception ex) ErrorCode = errorCode, ErrorMessage = errorMessage, StatusCode = statusCode, - Errors = errors + Errors = errors, }; return contentstackError; From 5b46c6daa4d8db60aee5de3dea04208082797a81 Mon Sep 17 00:00:00 2001 From: Nadeem Patwekar Date: Tue, 7 Oct 2025 17:18:02 +0530 Subject: [PATCH 2/3] feat: Update version to 2.25.0 and add VersionUtility for dynamic SDK version retrieval --- .talismanrc | 2 +- CHANGELOG.md | 9 ++ .../Internals/HttpRequestHandler.cs | 107 +++++++++++------- Contentstack.Core/Internals/VersionUtility.cs | 94 +++++++++++++++ Directory.Build.props | 2 +- 5 files changed, 173 insertions(+), 41 deletions(-) create mode 100644 Contentstack.Core/Internals/VersionUtility.cs diff --git a/.talismanrc b/.talismanrc index f993ff2..b1596ce 100644 --- a/.talismanrc +++ b/.talismanrc @@ -3,7 +3,7 @@ fileignoreconfig: ignore_detectors: - filecontent - filename: Contentstack.Core/Internals/HttpRequestHandler.cs - checksum: 94288e483056f41ff3a4c2ab652c3ce9ecb53dc0b9d4029456b34baed4f34891 + checksum: 62053e1b8772f44db054efc504d5d57f28fb7962c81021325854d478f570de09 - filename: Contentstack.Core/Models/Entry.cs checksum: 78a09b03b9fd6aefd0251353b2d8c70962bdfced16a6e1e28d10dc9af43da244 - filename: Contentstack.Core/ContentstackClient.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 171471c..b335753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +### Version: 2.25.0 +#### Date: Jan-07-2025 + +##### Feat: +- AssetLibrary + - Added new `Where` method for simple key-value pair queries + - Enhanced `Query` method to support multiple calls with intelligent merging +- Improved query handling with better null safety and error handling + ### Version: 2.24.0 #### Date: Sep-29-2025 diff --git a/Contentstack.Core/Internals/HttpRequestHandler.cs b/Contentstack.Core/Internals/HttpRequestHandler.cs index 85f8a10..8d36749 100644 --- a/Contentstack.Core/Internals/HttpRequestHandler.cs +++ b/Contentstack.Core/Internals/HttpRequestHandler.cs @@ -12,43 +12,56 @@ namespace Contentstack.Core.Internals { internal class HttpRequestHandler { - ContentstackClient client - { - get; set; - } + ContentstackClient client { get; set; } + internal HttpRequestHandler(ContentstackClient contentstackClient) { client = contentstackClient; } - public async Task ProcessRequest(string Url, Dictionary Headers, Dictionary BodyJson, string FileName = null, string Branch = null, bool isLivePreview = false, int timeout = 30000, WebProxy proxy = null) - { - String queryParam = String.Join("&", BodyJson.Select(kvp => { - var value = ""; - if (kvp.Value is string[]) + public async Task ProcessRequest( + string Url, + Dictionary Headers, + Dictionary BodyJson, + string FileName = null, + string Branch = null, + bool isLivePreview = false, + int timeout = 30000, + WebProxy proxy = null + ) + { + String queryParam = String.Join( + "&", + BodyJson.Select(kvp => { - string[] vals = (string[])kvp.Value; - value = String.Join("&", vals.Select(item => + var value = ""; + if (kvp.Value is string[]) { - return String.Format("{0}={1}", kvp.Key, item); - })); - return value; - } - else if (kvp.Value is Dictionary) - value = JsonConvert.SerializeObject(kvp.Value); - else - return String.Format("{0}={1}", kvp.Key, kvp.Value); - - return String.Format("{0}={1}", kvp.Key, value); + string[] vals = (string[])kvp.Value; + value = String.Join( + "&", + vals.Select(item => + { + return String.Format("{0}={1}", kvp.Key, item); + }) + ); + return value; + } + else if (kvp.Value is Dictionary) + value = JsonConvert.SerializeObject(kvp.Value); + else + return String.Format("{0}={1}", kvp.Key, kvp.Value); - })); + return String.Format("{0}={1}", kvp.Key, value); + }) + ); - var uri = new Uri(Url+"?"+queryParam); + var uri = new Uri(Url + "?" + queryParam); var request = (HttpWebRequest)WebRequest.Create(uri); request.Method = "GET"; request.ContentType = "application/json"; - request.Headers["x-user-agent"]="contentstack-delivery-dotnet/2.24.0"; + request.Headers["x-user-agent"] = VersionUtility.GetSdkVersion(); request.Timeout = timeout; if (proxy != null) @@ -60,52 +73,68 @@ public async Task ProcessRequest(string Url, Dictionary { request.Headers["branch"] = Branch; } - if (Headers != default(IDictionary)) { - foreach (var header in Headers) { - try { + if (Headers != default(IDictionary)) + { + foreach (var header in Headers) + { + try + { request.Headers[header.Key] = header.Value.ToString(); - } catch { - } + catch { } } } foreach (var plugin in client.Plugins) { request = await plugin.OnRequest(client, request); - }; + } + ; var serializedresult = JsonConvert.SerializeObject(BodyJson); byte[] requestBody = Encoding.UTF8.GetBytes(serializedresult); StreamReader reader = null; HttpWebResponse response = null; - try { + try + { response = (HttpWebResponse)await request.GetResponseAsync(); - if (response != null) { + if (response != null) + { reader = new StreamReader(response.GetResponseStream()); string responseString = await reader.ReadToEndAsync(); foreach (var plugin in client.Plugins) { - responseString = await plugin.OnResponse(client, request, response, responseString); + responseString = await plugin.OnResponse( + client, + request, + response, + responseString + ); } return responseString; - } else { + } + else + { return null; } - } catch (Exception we) { + } + catch (Exception we) + { throw we; - } finally { - if (reader != null) { + } + finally + { + if (reader != null) + { reader.Dispose(); } if (response != null) { - response.Dispose(); + response.Dispose(); } } - } //internal void updateLivePreviewContent(JObject response) diff --git a/Contentstack.Core/Internals/VersionUtility.cs b/Contentstack.Core/Internals/VersionUtility.cs new file mode 100644 index 0000000..c8c4003 --- /dev/null +++ b/Contentstack.Core/Internals/VersionUtility.cs @@ -0,0 +1,94 @@ +using System; +using System.Reflection; + +namespace Contentstack.Core.Internals +{ + /// + /// Utility class for handling SDK version information and user-agent string generation. + /// + internal static class VersionUtility + { + /// + /// Gets the SDK version dynamically from the assembly. + /// + /// The SDK version string in format: contentstack-delivery-dotnet/Major.Minor.Patch + public static string GetSdkVersion() + { + try + { + var assembly = Assembly.GetExecutingAssembly(); + var version = assembly.GetName().Version; + + // Check if version is valid (not 0.0.0.0 which is default for unversioned assemblies) + if (version != null && (version.Major > 0 || version.Minor > 0 || version.Build > 0)) + { + return $"contentstack-delivery-dotnet/{version.Major}.{version.Minor}.{version.Build}"; + } + + // Try to get version from assembly file version as fallback + var fileVersion = assembly.GetCustomAttribute()?.Version; + if (!string.IsNullOrEmpty(fileVersion)) + { + // Parse file version and extract only Major.Minor.Build (first 3 parts) + var versionParts = fileVersion.Split('.'); + if (versionParts.Length >= 3) + { + return $"contentstack-delivery-dotnet/{versionParts[0]}.{versionParts[1]}.{versionParts[2]}"; + } + return $"contentstack-delivery-dotnet/{fileVersion}"; + } + + // Try to get version from assembly informational version + var infoVersion = assembly.GetCustomAttribute()?.InformationalVersion; + if (!string.IsNullOrEmpty(infoVersion)) + { + // Extract semantic version (Major.Minor.Patch) from informational version + // Handle formats like "2.25.0", "2.25.0-beta.1", "2.25.0+abc123" + var semanticVersion = ExtractSemanticVersion(infoVersion); + if (!string.IsNullOrEmpty(semanticVersion)) + { + return $"contentstack-delivery-dotnet/{semanticVersion}"; + } + return $"contentstack-delivery-dotnet/{infoVersion}"; + } + } + catch + { + // Ignore exceptions and continue to fallback + } + + // Final fallback - use a generic identifier that doesn't imply a specific version + return "contentstack-delivery-dotnet/dev"; + } + + /// + /// Extracts semantic version (Major.Minor.Patch) from informational version string. + /// + /// The informational version string. + /// Semantic version in Major.Minor.Patch format. + private static string ExtractSemanticVersion(string informationalVersion) + { + try + { + // Remove build metadata (everything after +) + var versionWithoutMetadata = informationalVersion.Split('+')[0]; + + // Split by dots to get version parts + var parts = versionWithoutMetadata.Split('.'); + + // Ensure we have at least 3 parts (Major.Minor.Patch) + if (parts.Length >= 3) + { + // Take only the first 3 parts (Major.Minor.Patch) + return $"{parts[0]}.{parts[1]}.{parts[2]}"; + } + + return null; + } + catch + { + return null; + } + } + } +} diff --git a/Directory.Build.props b/Directory.Build.props index 2263977..3645501 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 2.24.0 + 2.25.0 From 83a6504d9ba2a864ca9a99786a1e3e8427853986 Mon Sep 17 00:00:00 2001 From: Nadeem Patwekar Date: Tue, 7 Oct 2025 17:50:53 +0530 Subject: [PATCH 3/3] feat: Add tests for VersionUtility methods and update copyright year to 2025 --- .../Contentstack.AspNetCore.csproj | 2 +- Contentstack.Core.Tests/VersionUtilityTest.cs | 379 ++++++++++++++++++ Contentstack.Core/Contentstack.Core.csproj | 7 +- Contentstack.Core/Internals/VersionUtility.cs | 56 ++- Contentstack.Core/LICENSE.txt | 2 +- 5 files changed, 431 insertions(+), 15 deletions(-) create mode 100644 Contentstack.Core.Tests/VersionUtilityTest.cs diff --git a/Contentstack.AspNetCore/Contentstack.AspNetCore.csproj b/Contentstack.AspNetCore/Contentstack.AspNetCore.csproj index 4d960fc..b71bafa 100644 --- a/Contentstack.AspNetCore/Contentstack.AspNetCore.csproj +++ b/Contentstack.AspNetCore/Contentstack.AspNetCore.csproj @@ -8,7 +8,7 @@ Contentstack $(Version) Main release - Copyright (c) 2012-2024 Contentstack (http://app.contentstack.com). All Rights Reserved + Copyright (c) 2012-2025 Contentstack (http://app.contentstack.com). All Rights Reserved https://github.com/contentstack/contentstack-dotnet v$(Version) $(Version) diff --git a/Contentstack.Core.Tests/VersionUtilityTest.cs b/Contentstack.Core.Tests/VersionUtilityTest.cs new file mode 100644 index 0000000..8811807 --- /dev/null +++ b/Contentstack.Core.Tests/VersionUtilityTest.cs @@ -0,0 +1,379 @@ +using System; +using System.Reflection; +using Contentstack.Core.Internals; +using Xunit; + +namespace Contentstack.Core.Tests +{ + public class VersionUtilityTest + { + #region GetSdkVersion Tests + + [Fact] + public void GetSdkVersion_ReturnsValidFormat() + { + // Act + var version = VersionUtility.GetSdkVersion(); + + // Assert + Assert.NotNull(version); + Assert.StartsWith("contentstack-delivery-dotnet/", version); + Assert.True(version.Length > "contentstack-delivery-dotnet/".Length); + } + + [Fact] + public void GetSdkVersion_ReturnsConsistentResult() + { + // Act + var version1 = VersionUtility.GetSdkVersion(); + var version2 = VersionUtility.GetSdkVersion(); + + // Assert + Assert.Equal(version1, version2); + } + + [Fact] + public void GetSdkVersion_DoesNotReturnNull() + { + // Act + var version = VersionUtility.GetSdkVersion(); + + // Assert + Assert.NotNull(version); + Assert.NotEmpty(version); + } + + [Fact] + public void GetSdkVersion_DoesNotReturnEmptyString() + { + // Act + var version = VersionUtility.GetSdkVersion(); + + // Assert + Assert.NotEmpty(version); + } + + [Fact] + public void GetSdkVersion_ContainsExpectedPrefix() + { + // Act + var version = VersionUtility.GetSdkVersion(); + + // Assert + Assert.StartsWith("contentstack-delivery-dotnet/", version); + } + + [Fact] + public void GetSdkVersion_DoesNotContainSpaces() + { + // Act + var version = VersionUtility.GetSdkVersion(); + + // Assert + Assert.DoesNotContain(" ", version); + } + + [Fact] + public void GetSdkVersion_DoesNotContainNewlines() + { + // Act + var version = VersionUtility.GetSdkVersion(); + + // Assert + Assert.DoesNotContain("\n", version); + Assert.DoesNotContain("\r", version); + } + + #endregion + + #region ExtractSemanticVersion Tests (via Reflection) + + [Theory] + [InlineData("1.2.3", "1.2.3")] + [InlineData("1.2.3-beta.1", "1.2.3-beta")] + [InlineData("1.2.3+abc123", "1.2.3")] + [InlineData("1.2.3-beta.1+abc123", "1.2.3-beta")] + [InlineData("2.25.0", "2.25.0")] + [InlineData("2.25.0-beta.1", "2.25.0-beta")] + [InlineData("2.25.0+abc123", "2.25.0")] + [InlineData("2.25.0-beta.1+abc123", "2.25.0-beta")] + [InlineData("10.20.30", "10.20.30")] + [InlineData("0.1.0", "0.1.0")] + public void ExtractSemanticVersion_ValidInputs_ReturnsCorrectVersion(string input, string expected) + { + // Arrange + var method = typeof(VersionUtility).GetMethod("ExtractSemanticVersion", BindingFlags.NonPublic | BindingFlags.Static); + + // Act + var result = method.Invoke(null, new object[] { input }) as string; + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("1.2")] + [InlineData("1")] + [InlineData("")] + [InlineData("invalid")] + [InlineData(" ")] + [InlineData("\t")] + [InlineData("\n")] + [InlineData("\r\n")] + public void ExtractSemanticVersion_InvalidInputs_ReturnsNull(string input) + { + // Arrange + var method = typeof(VersionUtility).GetMethod("ExtractSemanticVersion", BindingFlags.NonPublic | BindingFlags.Static); + + // Act + var result = method.Invoke(null, new object[] { input }) as string; + + // Assert + Assert.Null(result); + } + + [Fact] + public void ExtractSemanticVersion_NullInput_ReturnsNull() + { + // Arrange + var method = typeof(VersionUtility).GetMethod("ExtractSemanticVersion", BindingFlags.NonPublic | BindingFlags.Static); + + // Act + var result = method.Invoke(null, new object[] { null }) as string; + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("1.2.3+", "1.2.3")] // Should handle trailing + correctly + [InlineData(" 1.2.3 ", "1.2.3")] // Should handle whitespace + [InlineData("1.2.3.4.5", "1.2.3")] // Should handle extra version parts + public void ExtractSemanticVersion_ImprovedHandling_WorksCorrectly(string input, string expected) + { + // Arrange + var method = typeof(VersionUtility).GetMethod("ExtractSemanticVersion", BindingFlags.NonPublic | BindingFlags.Static); + + // Act + var result = method.Invoke(null, new object[] { input }) as string; + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("1.2.3.4", "1.2.3")] // Should take only first 3 parts + [InlineData("1.2.3.4.5.6", "1.2.3")] // Should take only first 3 parts + [InlineData("1.2.3.4.5", "1.2.3")] // Should take only first 3 parts + public void ExtractSemanticVersion_MoreThanThreeParts_TakesFirstThree(string input, string expected) + { + // Arrange + var method = typeof(VersionUtility).GetMethod("ExtractSemanticVersion", BindingFlags.NonPublic | BindingFlags.Static); + + // Act + var result = method.Invoke(null, new object[] { input }) as string; + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("1.2.3+metadata", "1.2.3")] + [InlineData("1.2.3-beta.1+metadata", "1.2.3-beta")] + [InlineData("1.2.3+very-long-metadata-string", "1.2.3")] + [InlineData("1.2.3+", "1.2.3")] + public void ExtractSemanticVersion_WithBuildMetadata_RemovesMetadata(string input, string expected) + { + // Arrange + var method = typeof(VersionUtility).GetMethod("ExtractSemanticVersion", BindingFlags.NonPublic | BindingFlags.Static); + + // Act + var result = method.Invoke(null, new object[] { input }) as string; + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("1.2.3-beta.1", "1.2.3-beta")] + [InlineData("1.2.3-alpha.1", "1.2.3-alpha")] + [InlineData("1.2.3-rc.1", "1.2.3-rc")] + [InlineData("1.2.3-preview.1", "1.2.3-preview")] + public void ExtractSemanticVersion_WithPreReleaseIdentifiers_KeepsPreRelease(string input, string expected) + { + // Arrange + var method = typeof(VersionUtility).GetMethod("ExtractSemanticVersion", BindingFlags.NonPublic | BindingFlags.Static); + + // Act + var result = method.Invoke(null, new object[] { input }) as string; + + // Assert + Assert.Equal(expected, result); + } + + #endregion + + #region Edge Cases and Error Scenarios + + [Fact] + public void GetSdkVersion_HandlesExceptions_Gracefully() + { + // This test ensures that GetSdkVersion doesn't throw exceptions + // and returns a fallback value when assembly reflection fails + + // Act & Assert - should not throw + var version = VersionUtility.GetSdkVersion(); + Assert.NotNull(version); + Assert.NotEmpty(version); + } + + [Fact] + public void GetSdkVersion_ReturnsFallbackWhenAssemblyVersionIsInvalid() + { + // This test verifies that when assembly version is 0.0.0.0 or invalid, + // the method falls back to other version sources or returns "dev" + + // Act + var version = VersionUtility.GetSdkVersion(); + + // Assert + Assert.NotNull(version); + Assert.True(version == "contentstack-delivery-dotnet/dev" || + version.StartsWith("contentstack-delivery-dotnet/")); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + [InlineData("\t")] + [InlineData("\n")] + [InlineData("\r\n")] + public void ExtractSemanticVersion_WhitespaceInputs_ReturnsNull(string input) + { + // Arrange + var method = typeof(VersionUtility).GetMethod("ExtractSemanticVersion", BindingFlags.NonPublic | BindingFlags.Static); + + // Act + var result = method.Invoke(null, new object[] { input }) as string; + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("1.2.3.4.5.6.7.8.9.10")] // Very long version + [InlineData("999999999.999999999.999999999")] // Very large numbers + [InlineData("0.0.0")] // All zeros + public void ExtractSemanticVersion_EdgeCaseInputs_HandlesCorrectly(string input) + { + // Arrange + var method = typeof(VersionUtility).GetMethod("ExtractSemanticVersion", BindingFlags.NonPublic | BindingFlags.Static); + + // Act + var result = method.Invoke(null, new object[] { input }) as string; + + // Assert + if (input == "0.0.0") + { + Assert.Equal("0.0.0", result); + } + else + { + Assert.NotNull(result); + Assert.True(result.Split('.').Length == 3); + } + } + + [Fact] + public void ExtractSemanticVersion_HandlesExceptions_Gracefully() + { + // This test ensures that ExtractSemanticVersion doesn't throw exceptions + // and returns null when parsing fails + + // Arrange + var method = typeof(VersionUtility).GetMethod("ExtractSemanticVersion", BindingFlags.NonPublic | BindingFlags.Static); + + // Act & Assert - should not throw + var result = method.Invoke(null, new object[] { "invalid-version-string" }) as string; + Assert.Null(result); + } + + #endregion + + #region Integration Tests + + [Fact] + public void GetSdkVersion_Integration_ReturnsValidUserAgentFormat() + { + // Act + var version = VersionUtility.GetSdkVersion(); + + // Assert + Assert.NotNull(version); + Assert.StartsWith("contentstack-delivery-dotnet/", version); + + // Verify it's in a format suitable for User-Agent headers + Assert.DoesNotContain(" ", version); + Assert.DoesNotContain("\n", version); + Assert.DoesNotContain("\r", version); + Assert.DoesNotContain("\t", version); + } + + [Fact] + public void GetSdkVersion_Integration_CanBeUsedInHttpHeaders() + { + // Act + var version = VersionUtility.GetSdkVersion(); + + // Assert + // Verify the version string is suitable for HTTP headers + Assert.NotNull(version); + Assert.NotEmpty(version); + + // Should not contain characters that would break HTTP headers + Assert.DoesNotContain("\"", version); + Assert.DoesNotContain("'", version); + Assert.DoesNotContain("\n", version); + Assert.DoesNotContain("\r", version); + } + + #endregion + + #region Performance Tests + + [Fact] + public void GetSdkVersion_Performance_ReturnsQuickly() + { + // Act & Assert + var startTime = DateTime.UtcNow; + var version = VersionUtility.GetSdkVersion(); + var endTime = DateTime.UtcNow; + + // Should complete quickly (less than 1 second) + var duration = endTime - startTime; + Assert.True(duration.TotalSeconds < 1, $"GetSdkVersion took {duration.TotalSeconds} seconds"); + Assert.NotNull(version); + } + + [Fact] + public void GetSdkVersion_Performance_MultipleCalls_Consistent() + { + // Act + var versions = new string[100]; + for (int i = 0; i < 100; i++) + { + versions[i] = VersionUtility.GetSdkVersion(); + } + + // Assert + var firstVersion = versions[0]; + for (int i = 1; i < versions.Length; i++) + { + Assert.Equal(firstVersion, versions[i]); + } + } + + #endregion + } +} diff --git a/Contentstack.Core/Contentstack.Core.csproj b/Contentstack.Core/Contentstack.Core.csproj index 63bf128..2ac53aa 100644 --- a/Contentstack.Core/Contentstack.Core.csproj +++ b/Contentstack.Core/Contentstack.Core.csproj @@ -8,7 +8,7 @@ $(Version) Contentstack Reference in entry Live preview support added - Copyright © 2012-2024 Contentstack. All Rights Reserved + Copyright © 2012-2025 Contentstack. All Rights Reserved true v$(Version) https://github.com/contentstack/contentstack-dotnet @@ -54,4 +54,9 @@ + + + <_Parameter1>Contentstack.Core.Tests + + diff --git a/Contentstack.Core/Internals/VersionUtility.cs b/Contentstack.Core/Internals/VersionUtility.cs index c8c4003..16d8172 100644 --- a/Contentstack.Core/Internals/VersionUtility.cs +++ b/Contentstack.Core/Internals/VersionUtility.cs @@ -18,15 +18,20 @@ public static string GetSdkVersion() { var assembly = Assembly.GetExecutingAssembly(); var version = assembly.GetName().Version; - + // Check if version is valid (not 0.0.0.0 which is default for unversioned assemblies) - if (version != null && (version.Major > 0 || version.Minor > 0 || version.Build > 0)) + if ( + version != null + && (version.Major > 0 || version.Minor > 0 || version.Build > 0) + ) { return $"contentstack-delivery-dotnet/{version.Major}.{version.Minor}.{version.Build}"; } - + // Try to get version from assembly file version as fallback - var fileVersion = assembly.GetCustomAttribute()?.Version; + var fileVersion = assembly + .GetCustomAttribute() + ?.Version; if (!string.IsNullOrEmpty(fileVersion)) { // Parse file version and extract only Major.Minor.Build (first 3 parts) @@ -37,9 +42,11 @@ public static string GetSdkVersion() } return $"contentstack-delivery-dotnet/{fileVersion}"; } - + // Try to get version from assembly informational version - var infoVersion = assembly.GetCustomAttribute()?.InformationalVersion; + var infoVersion = assembly + .GetCustomAttribute() + ?.InformationalVersion; if (!string.IsNullOrEmpty(infoVersion)) { // Extract semantic version (Major.Minor.Patch) from informational version @@ -56,7 +63,7 @@ public static string GetSdkVersion() { // Ignore exceptions and continue to fallback } - + // Final fallback - use a generic identifier that doesn't imply a specific version return "contentstack-delivery-dotnet/dev"; } @@ -70,19 +77,44 @@ private static string ExtractSemanticVersion(string informationalVersion) { try { + // Handle null or empty input + if (string.IsNullOrWhiteSpace(informationalVersion)) + { + return null; + } + // Remove build metadata (everything after +) var versionWithoutMetadata = informationalVersion.Split('+')[0]; - + + // Handle case where version ends with + (e.g., "1.2.3+") + if (string.IsNullOrWhiteSpace(versionWithoutMetadata)) + { + return null; + } + // Split by dots to get version parts var parts = versionWithoutMetadata.Split('.'); - + // Ensure we have at least 3 parts (Major.Minor.Patch) if (parts.Length >= 3) { - // Take only the first 3 parts (Major.Minor.Patch) - return $"{parts[0]}.{parts[1]}.{parts[2]}"; + // Validate that all parts are numeric or contain valid version identifiers + var major = parts[0].Trim(); + var minor = parts[1].Trim(); + var patch = parts[2].Trim(); + + // Check if we have valid version components + if ( + !string.IsNullOrEmpty(major) + && !string.IsNullOrEmpty(minor) + && !string.IsNullOrEmpty(patch) + ) + { + // Take only the first 3 parts (Major.Minor.Patch) + return $"{major}.{minor}.{patch}"; + } } - + return null; } catch diff --git a/Contentstack.Core/LICENSE.txt b/Contentstack.Core/LICENSE.txt index ec1403b..e1238b5 100644 --- a/Contentstack.Core/LICENSE.txt +++ b/Contentstack.Core/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2012-2024 Contentstack (http://app.contentstack.com). All Rights Reserved +Copyright (c) 2012-2025 Contentstack (http://app.contentstack.com). All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal