diff --git a/.cursor/rules/README.md b/.cursor/rules/README.md deleted file mode 100644 index f5c1f870..00000000 --- a/.cursor/rules/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Cursor (optional) - -**Cursor** users: start at **[AGENTS.md](../../AGENTS.md)**. All conventions live in **`skills/*/SKILL.md`**. - -This folder only points contributors to **`AGENTS.md`** so editor-specific config does not duplicate the canonical docs. diff --git a/.github/workflows/back-merge-pr.yml b/.github/workflows/back-merge-pr.yml deleted file mode 100644 index 02b378ce..00000000 --- a/.github/workflows/back-merge-pr.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Back-merge master to development - -on: - push: - branches: - - master - workflow_dispatch: - -permissions: - contents: read - pull-requests: write - -jobs: - open-back-merge-pr: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Open back-merge PR if needed - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - BASE_BRANCH="development" - SOURCE_BRANCH="master" - - git fetch origin "$BASE_BRANCH" "$SOURCE_BRANCH" - - if ! git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH"; then - echo "Base branch '$BASE_BRANCH' does not exist on origin; skipping." - exit 0 - fi - - SOURCE_SHA=$(git rev-parse "origin/$SOURCE_BRANCH") - BASE_SHA=$(git rev-parse "origin/$BASE_BRANCH") - - if [ "$SOURCE_SHA" = "$BASE_SHA" ]; then - echo "$SOURCE_BRANCH and $BASE_BRANCH are at the same commit; nothing to back-merge." - exit 0 - fi - - EXISTING=$(gh pr list --repo "${{ github.repository }}" --base "$BASE_BRANCH" --head "$SOURCE_BRANCH" --state open --json number --jq 'length') - - if [ "$EXISTING" -gt 0 ]; then - echo "An open PR from $SOURCE_BRANCH to $BASE_BRANCH already exists; skipping." - exit 0 - fi - - gh pr create --repo "${{ github.repository }}" --base "$BASE_BRANCH" --head "$SOURCE_BRANCH" --title "chore: back-merge $SOURCE_BRANCH into $BASE_BRANCH" --body "Automated back-merge after changes landed on \\`$SOURCE_BRANCH\\`. Review and merge to keep \\`$BASE_BRANCH\\` in sync." - - echo "Created back-merge PR $SOURCE_BRANCH -> $BASE_BRANCH." diff --git a/.github/workflows/check-branch.yml b/.github/workflows/check-branch.yml new file mode 100644 index 00000000..856c7181 --- /dev/null +++ b/.github/workflows/check-branch.yml @@ -0,0 +1,18 @@ +name: 'Check Branch' + +on: + pull_request: + +jobs: + check_branch: + runs-on: ubuntu-latest + steps: + # PRs into master are no longer blocked here based on their source branch. + # Direct merges into master (i.e. without going through PR review) are expected + # to be prevented by GitHub branch protection rules on master, not by this CI check. + - name: Branch check (informational only) + run: | + echo "This workflow no longer restricts which branch a PR into master can come from." + echo "Base branch : ${{ github.base_ref }}" + echo "Head branch : ${{ github.head_ref }}" + echo "Direct pushes/merges to master should be prevented via branch protection rules, not this workflow." diff --git a/.github/workflows/check-version-bump.yml b/.github/workflows/check-version-bump.yml deleted file mode 100644 index 8e710002..00000000 --- a/.github/workflows/check-version-bump.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Check Version Bump - -on: - pull_request: - -jobs: - version-bump: - name: Version & Changelog bump - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Detect changed files and version bump - id: detect - run: | - if git rev-parse HEAD^2 >/dev/null 2>&1; then - FILES=$(git diff --name-only HEAD^1 HEAD^2) - else - FILES=$(git diff --name-only HEAD~1 HEAD) - fi - VERSION_FILES_CHANGED=false - echo "$FILES" | grep -qx 'package.json' && VERSION_FILES_CHANGED=true - echo "$FILES" | grep -qx 'CHANGELOG.md' && VERSION_FILES_CHANGED=true - echo "version_files_changed=$VERSION_FILES_CHANGED" >> $GITHUB_OUTPUT - # Only lib/, webpack/, dist/, package.json count as release-affecting; .github/ and test/ do not - CODE_CHANGED=false - echo "$FILES" | grep -qE '^lib/|^webpack/|^dist/' && CODE_CHANGED=true - echo "$FILES" | grep -qx 'package.json' && CODE_CHANGED=true - echo "code_changed=$CODE_CHANGED" >> $GITHUB_OUTPUT - - - name: Skip when only test/docs/.github changed - if: steps.detect.outputs.code_changed != 'true' - run: | - echo "No release-affecting files changed (e.g. only test/docs/.github). Skipping version-bump check." - exit 0 - - - name: Fail when version bump was missed - if: steps.detect.outputs.code_changed == 'true' && steps.detect.outputs.version_files_changed != 'true' - run: | - echo "::error::This PR has code changes but no version bump. Please bump the version in package.json and add an entry in CHANGELOG.md." - exit 1 - - - name: Setup Node - if: steps.detect.outputs.code_changed == 'true' && steps.detect.outputs.version_files_changed == 'true' - uses: actions/setup-node@v4 - with: - node-version: '22.x' - - - name: Check version bump - if: steps.detect.outputs.code_changed == 'true' && steps.detect.outputs.version_files_changed == 'true' - run: | - set -e - PKG_VERSION=$(node -p "require('./package.json').version.replace(/^v/, '')") - if [ -z "$PKG_VERSION" ]; then - echo "::error::Could not read version from package.json" - exit 1 - fi - git fetch --tags --force 2>/dev/null || true - LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || true) - if [ -z "$LATEST_TAG" ]; then - echo "No existing tags found. Skipping version-bump check (first release)." - exit 0 - fi - LATEST_VERSION="${LATEST_TAG#v}" - LATEST_VERSION="${LATEST_VERSION%%-*}" - if [ "$(printf '%s\n' "$LATEST_VERSION" "$PKG_VERSION" | sort -V | tail -1)" != "$PKG_VERSION" ]; then - echo "::error::Version bump required: package.json version ($PKG_VERSION) is not greater than latest tag ($LATEST_TAG). Please bump the version in package.json." - exit 1 - fi - if [ "$PKG_VERSION" = "$LATEST_VERSION" ]; then - echo "::error::Version bump required: package.json version ($PKG_VERSION) equals latest tag ($LATEST_TAG). Please bump the version in package.json." - exit 1 - fi - CHANGELOG_VERSION=$(sed -nE 's/^## \[v?([0-9]+\.[0-9]+\.[0-9]+).*/\1/p' CHANGELOG.md | head -1) - if [ -z "$CHANGELOG_VERSION" ]; then - echo "::error::Could not find a version entry in CHANGELOG.md (expected line like '## [v1.0.0](...)')." - exit 1 - fi - if [ "$CHANGELOG_VERSION" != "$PKG_VERSION" ]; then - echo "::error::CHANGELOG version mismatch: CHANGELOG.md top version ($CHANGELOG_VERSION) does not match package.json version ($PKG_VERSION). Please add or update the CHANGELOG entry for $PKG_VERSION." - exit 1 - fi - echo "Version bump check passed: package.json and CHANGELOG.md are at $PKG_VERSION (latest tag: $LATEST_TAG)." diff --git a/.github/workflows/sca-scan.yml b/.github/workflows/sca-scan.yml index 21e97963..274c89b8 100644 --- a/.github/workflows/sca-scan.yml +++ b/.github/workflows/sca-scan.yml @@ -13,7 +13,7 @@ jobs: - name: Setup .NET Core @ Latest uses: actions/setup-dotnet@v1 with: - dotnet-version: "7.0.x" + dotnet-version: "10.0.x" - name: Run Dotnet Restore run: dotnet restore diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index bd228aca..dec61dd6 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -9,10 +9,10 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4.2.2 - - name: Setup .NET 7.0 + - name: Setup .NET uses: actions/setup-dotnet@v4.3.0 with: - dotnet-version: '7.0.x' + dotnet-version: '10.0.x' - name: Restore dependencies run: dotnet restore Contentstack.Net.sln - name: Build solution diff --git a/.gitignore b/.gitignore index 152183fd..9d4a4f27 100644 --- a/.gitignore +++ b/.gitignore @@ -67,4 +67,6 @@ packages/ *.sln.docstates # Python -Scripts/venv/ \ No newline at end of file +Scripts/venv/ +# Cached region registry — regenerated at runtime from CDN +*/Assets/regions.json diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index d8381ca1..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,49 +0,0 @@ -# Contentstack .NET SDK – Agent guide - -**Universal entry point** for contributors and AI agents. Each skill is documented in **`skills/*/SKILL.md`** (YAML frontmatter for agent discovery where applicable). - -## What this repo is - -| Field | Detail | -|-------|--------| -| **Name:** | [contentstack-dotnet](https://github.com/contentstack/contentstack-dotnet) | -| **Purpose:** | .NET SDK for Contentstack’s Content Delivery API (CDA)—fetch entries, assets, and run queries from .NET apps. | -| **Out of scope (if any):** | Do not bypass the SDK HTTP layer with ad-hoc `HttpClient` usage; all requests go through `HttpRequestHandler` (see `skills/sdk-core-patterns/SKILL.md`). | - -## Tech stack (at a glance) - -| Area | Details | -|------|---------| -| Language | C#; multi-targeting includes `netstandard2.0`, `net47`, `net472` (see project files under `Contentstack.Core/`). | -| Build | .NET SDK — solution [`Contentstack.Net.sln`](Contentstack.Net.sln); packages [`Contentstack.Core/`](Contentstack.Core/) (Delivery SDK), [`Contentstack.AspNetCore/`](Contentstack.AspNetCore/) (DI extensions). | -| Tests | xUnit; unit tests in [`Contentstack.Core.Unit.Tests/`](Contentstack.Core.Unit.Tests/) (no credentials); integration tests in [`Contentstack.Core.Tests/`](Contentstack.Core.Tests/) (requires `app.config` / API credentials). | -| Lint / coverage | No dedicated repo-wide lint/format CLI in CI. Security/static analysis: [CodeQL workflow](.github/workflows/codeql-analysis.yml). | -| Other | JSON: Newtonsoft.Json; package version: single source in [`Directory.Build.props`](Directory.Build.props). | - -## Commands (quick reference) - -| Command type | Command | -|--------------|---------| -| Build | `dotnet build Contentstack.Net.sln` | -| Test (unit) | `dotnet test Contentstack.Core.Unit.Tests/Contentstack.Core.Unit.Tests.csproj` | -| Test (integration) | `dotnet test Contentstack.Core.Tests/Contentstack.Core.Tests.csproj` (configure credentials locally) | - -CI: [`.github/workflows/unit-test.yml`](.github/workflows/unit-test.yml) restores, builds, and runs unit tests on Windows (.NET 7). Other workflows include [NuGet publish](.github/workflows/nuget-publish.yml), [branch checks](.github/workflows/check-branch.yml), [CodeQL](.github/workflows/codeql-analysis.yml), policy/SCA scans. - -## Where the documentation lives: skills - -| Skill | Path | What it covers | -|-------|------|----------------| -| Dev workflow | [`skills/dev-workflow/SKILL.md`](skills/dev-workflow/SKILL.md) | Solution layout, build/test commands, versioning, CI entry points. | -| SDK core patterns | [`skills/sdk-core-patterns/SKILL.md`](skills/sdk-core-patterns/SKILL.md) | Architecture, `ContentstackClient`, HTTP layer, DI, plugins. | -| Query building | [`skills/query-building/SKILL.md`](skills/query-building/SKILL.md) | Fluent query API, operators, pagination, sync, taxonomy. | -| Models and serialization | [`skills/models-and-serialization/SKILL.md`](skills/models-and-serialization/SKILL.md) | Entry/Asset models, JSON converters, collections. | -| Error handling | [`skills/error-handling/SKILL.md`](skills/error-handling/SKILL.md) | Exception hierarchy, `ErrorMessages`, API error parsing. | -| Testing | [`skills/testing/SKILL.md`](skills/testing/SKILL.md) | Unit vs integration tests, AutoFixture, `IntegrationTestBase`. | -| Code review | [`skills/code-review/SKILL.md`](skills/code-review/SKILL.md) | PR checklist for this SDK. | - -An index with “when to use” hints is in [`skills/README.md`](skills/README.md). - -## Using Cursor (optional) - -If you use **Cursor**, [`.cursor/rules/README.md`](.cursor/rules/README.md) only points to **`AGENTS.md`**—the same conventions as for everyone else. Canonical guidance remains in **`skills/*/SKILL.md`**. diff --git a/CHANGELOG.md b/CHANGELOG.md index c07e91a4..f5b50d4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,82 @@ +### Version: 3.0.0 +#### Date: Jul-13-2026 + +##### Breaking Changes: +- Removed `Newtonsoft.Json` dependency; all JSON serialisation now uses `System.Text.Json` (BCL) +- `Entry.ToJson()`, `Query.Count()`, `AssetLibrary.Count()` now return `JsonObject` instead of `JObject` +- `AssetLibrary.Query(JsonObject)` — parameter type changed from `JObject` to `JsonObject` +- `ContentType.Fetch()`, `GlobalField.Fetch()`, `GlobalFieldQuery.Find()` now return `JsonObject` instead of `JObject` +- `SerializerSettings` → `SerializerOptions` +- Model classes use `[JsonPropertyName]` instead of `[JsonProperty]` +- Requires **.NET 10** or later +- Updated `contentstack.utils` dependency to `2.0.0` (final, non-beta) + +##### Feat: +- Added `Endpoint` class for dynamic region-to-URL resolution via CDN-backed `regions.json` +- Added `ContentstackRegionMap` to map `ContentstackRegion` enum to registry region IDs +- Added `GCP_EU` region support +- Added `ApiErrorBodyParser` for consistent API error envelope parsing +- Added `JsonNodeConversion` and `JsonObjectMerge` utilities to replace Newtonsoft equivalents +- Added `ContentstackJsonDefaults` — shared `JsonSerializerOptions` used across all custom converters + +##### Enh: +- `Config.BaseUrl` now resolves hosts from the regions registry; removed hardcoded `regionCode()` and `HostURL` +- Replaced `Console.WriteLine` with `Debug.WriteLine` in `ContentstackConvert` to suppress parse warnings from application stdout + +##### Chore: +- Replaced `refresh-region.cs` with `refresh-region.py` — avoids MSBuild compiling the script as source +- Added `build/contentstack.csharp.targets` to auto-deliver `refresh-region.py` to consumer projects on first build +- Added `Assets/regions.json` to `.gitignore` +- Added `EndpointTest.cs` +- Updated .NET version in SCA scan CI from `7.0.x` to `10.0.x` + +##### Migration Guide: +- See [Migrating from Newtonsoft.Json to System.Text.Json](https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/dot-net/migrate-dotnet-delivery-sdk-from-newtonsoft.json-to-system.text.json) for the full upgrade path from v2.x. + +--- + +### Version: 3.0.0-beta.2 +#### Date: Jun-22-2026 + +##### Feat: +- Added `Endpoint` class for dynamic region-to-URL resolution via CDN-backed `regions.json` +- Added `ContentstackRegionMap` to map `ContentstackRegion` enum to registry region IDs +- Added `GCP_EU` region support + +##### Enh: +- `Config.BaseUrl` now resolves hosts from the regions registry; removed hardcoded `regionCode()` and `HostURL` + +##### Chore: +- Replaced `refresh-region.cs` with `refresh-region.py` — avoids MSBuild compiling the script as source +- Added `build/contentstack.csharp.targets` to auto-deliver `refresh-region.py` to consumer projects on first build +- Added `Assets/regions.json` to `.gitignore` +- Added `EndpointTest.cs` + +--- + +### Version: 3.0.0-beta.1 +#### Date: May-04-2026 + +##### Breaking Changes: +- Removed `Newtonsoft.Json` dependency; all JSON serialisation now uses `System.Text.Json` (BCL) + - `AssetJsonConverter` and `EntryJsonConverter` now implement `System.Text.Json.Serialization.JsonConverter` + - `ContentstackCollection` no longer carries `[JsonObject]`; direct `JsonSerializer` usage on this type is not supported +- Updated `contentstack.utils` from `1.0.6` to `2.0.0-beta.1` (major version bump) + +##### Feat: +- Migrated all internal JSON handling to `System.Text.Json` +- Added `ApiErrorBodyParser` for consistent API error envelope parsing +- Added `JsonNodeConversion` and `JsonObjectMerge` utilities to replace Newtonsoft equivalents +- Added `ContentstackJsonDefaults` — shared `JsonSerializerOptions` used across all custom converters + +##### Enh: +- Replaced `Console.WriteLine` with `Debug.WriteLine` in `ContentstackConvert` to suppress parse warnings from application stdout + +##### Chore: +- Updated .NET version in SCA scan CI from `7.0.x` to `10.0.x` + +--- + ### Version: 2.28.0 #### Date: Jun-24-2026 @@ -5,6 +84,8 @@ - Register `EmbeddedObjectConverter` in `ContentstackClient` constructor so `.includeEmbeddedItems().Fetch()` deserializes `_embedded_items` correctly when the model implements `IEntryEmbedable`. No changes required in consumer code. - Upgraded utils dependency from `contentstack.utils 1.3.0` to `contentstack.utils 1.4.0` which ships the concrete `EmbeddedObject` class and `EmbeddedObjectConverter`. +--- + ### Version: 2.27.0 #### Date: Apr-23-2026 @@ -25,6 +106,8 @@ - Comprehensive error handling tests for Timeline Preview edge cases +--- + ### Version: 2.26.0 #### Date: Feb-10-2026 diff --git a/Contentstack.Core.Tests/Contentstack.Core.Tests.csproj b/Contentstack.Core.Tests/Contentstack.Core.Tests.csproj index f3863dd7..4dcac804 100644 --- a/Contentstack.Core.Tests/Contentstack.Core.Tests.csproj +++ b/Contentstack.Core.Tests/Contentstack.Core.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net10.0 false $(Version) @@ -27,20 +27,9 @@ - - - - - - - - - ..\Contentstack.Core\bin\Debug\Contentstack.Core.dll - - @@ -70,4 +59,4 @@ Always - \ No newline at end of file + diff --git a/Contentstack.Core.Tests/EndpointTest.cs b/Contentstack.Core.Tests/EndpointTest.cs new file mode 100644 index 00000000..ad64e16c --- /dev/null +++ b/Contentstack.Core.Tests/EndpointTest.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Contentstack.Core.Endpoints; +using Xunit; + +namespace Contentstack.Core.Tests +{ + /// + /// Tests for — dynamic CDN-backed region resolution. + /// Mirrors the coverage from contentstack-utils-dotnet PR #66 EndpointTest.cs, + /// adapted for the Delivery SDK (contentDelivery as primary service key). + /// + public class EndpointTest : IDisposable + { + public EndpointTest() + { + // Each test starts with a clean cache so CDN/disk state doesn't bleed across tests. + Endpoint.ResetCache(); + } + + public void Dispose() + { + Endpoint.ResetCache(); + } + + // ── Basic resolution ────────────────────────────────────────────────── + + [Fact] + public void GetContentstackEndpoint_Na_ReturnsCorrectCdnUrl() + { + string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + Assert.Equal("https://cdn.contentstack.io", url); + } + + [Fact] + public void GetContentstackEndpoint_Eu_ReturnsCorrectCdnUrl() + { + string url = Endpoint.GetContentstackEndpoint("eu", "contentDelivery"); + Assert.Equal("https://eu-cdn.contentstack.com", url); + } + + [Fact] + public void GetContentstackEndpoint_Au_ReturnsCorrectCdnUrl() + { + string url = Endpoint.GetContentstackEndpoint("au", "contentDelivery"); + Assert.Equal("https://au-cdn.contentstack.com", url); + } + + [Fact] + public void GetContentstackEndpoint_AzureNa_ReturnsCorrectCdnUrl() + { + string url = Endpoint.GetContentstackEndpoint("azure-na", "contentDelivery"); + Assert.Equal("https://azure-na-cdn.contentstack.com", url); + } + + [Fact] + public void GetContentstackEndpoint_AzureEu_ReturnsCorrectCdnUrl() + { + string url = Endpoint.GetContentstackEndpoint("azure-eu", "contentDelivery"); + Assert.Equal("https://azure-eu-cdn.contentstack.com", url); + } + + [Fact] + public void GetContentstackEndpoint_GcpNa_ReturnsCorrectCdnUrl() + { + string url = Endpoint.GetContentstackEndpoint("gcp-na", "contentDelivery"); + Assert.Equal("https://gcp-na-cdn.contentstack.com", url); + } + + [Fact] + public void GetContentstackEndpoint_GcpEu_ReturnsCorrectCdnUrl() + { + string url = Endpoint.GetContentstackEndpoint("gcp-eu", "contentDelivery"); + Assert.Equal("https://gcp-eu-cdn.contentstack.com", url); + } + + // ── NA alias resolution ─────────────────────────────────────────────── + + [Theory] + [InlineData("na")] + [InlineData("us")] + [InlineData("NA")] + [InlineData("US")] + [InlineData("AWS-NA")] + [InlineData("aws_na")] + [InlineData("AWS_NA")] + public void GetContentstackEndpoint_NaAliasVariants_AllResolveToSameCdn(string alias) + { + string url = Endpoint.GetContentstackEndpoint(alias, "contentDelivery"); + Assert.Equal("https://cdn.contentstack.io", url); + } + + // ── omitHttps flag ──────────────────────────────────────────────────── + + [Fact] + public void GetContentstackEndpoint_OmitHttps_ReturnsHostOnly() + { + string host = Endpoint.GetContentstackEndpoint("na", "contentDelivery", omitHttps: true); + Assert.Equal("cdn.contentstack.io", host); + } + + [Fact] + public void GetContentstackEndpoint_OmitHttps_Eu_ReturnsHostOnly() + { + string host = Endpoint.GetContentstackEndpoint("eu", "contentDelivery", omitHttps: true); + Assert.Equal("eu-cdn.contentstack.com", host); + } + + [Fact] + public void GetContentstackEndpoint_OmitHttpsFalse_ReturnsFullUrl() + { + string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery", omitHttps: false); + Assert.StartsWith("https://", url); + } + + // ── Management endpoint (also valid for Delivery SDK to know) ───────── + + [Fact] + public void GetContentstackEndpoint_Na_ContentManagement_ReturnsApiUrl() + { + string url = Endpoint.GetContentstackEndpoint("na", "contentManagement"); + Assert.Equal("https://api.contentstack.io", url); + } + + // ── Dictionary overload ─────────────────────────────────────────────── + + [Fact] + public void GetContentstackEndpoint_DictionaryOverload_Na_ContainsContentDelivery() + { + Dictionary all = Endpoint.GetContentstackEndpoint("na"); + Assert.True(all.ContainsKey("contentDelivery")); + Assert.Equal("https://cdn.contentstack.io", all["contentDelivery"]); + } + + [Fact] + public void GetContentstackEndpoint_DictionaryOverload_Na_HasExpectedKeyCount() + { + Dictionary all = Endpoint.GetContentstackEndpoint("na"); + Assert.True(all.Count >= 18, $"Expected at least 18 service keys, got {all.Count}"); + } + + [Fact] + public void GetContentstackEndpoint_DictionaryOverload_OmitHttps_AllValuesLackScheme() + { + Dictionary all = Endpoint.GetContentstackEndpoint("na", omitHttps: true); + foreach (var kvp in all) + Assert.DoesNotMatch(@"^https?://", kvp.Value); + } + + [Fact] + public void GetContentstackEndpoint_DictionaryOverload_Eu_ContainsContentDelivery() + { + Dictionary all = Endpoint.GetContentstackEndpoint("eu"); + Assert.True(all.ContainsKey("contentDelivery")); + } + + // ── Error handling ──────────────────────────────────────────────────── + + [Fact] + public void GetContentstackEndpoint_EmptyRegion_ThrowsArgumentException() + { + Assert.Throws(() => + Endpoint.GetContentstackEndpoint("", "contentDelivery")); + } + + [Fact] + public void GetContentstackEndpoint_WhitespaceRegion_ThrowsArgumentException() + { + Assert.Throws(() => + Endpoint.GetContentstackEndpoint(" ", "contentDelivery")); + } + + [Fact] + public void GetContentstackEndpoint_UnknownRegion_ThrowsKeyNotFoundException() + { + Assert.Throws(() => + Endpoint.GetContentstackEndpoint("invalid-region-xyz", "contentDelivery")); + } + + [Fact] + public void GetContentstackEndpoint_UnknownService_ThrowsKeyNotFoundException() + { + Assert.Throws(() => + Endpoint.GetContentstackEndpoint("na", "nonExistentService")); + } + + [Fact] + public void GetContentstackEndpoint_DictionaryOverload_EmptyRegion_ThrowsArgumentException() + { + Assert.Throws(() => + Endpoint.GetContentstackEndpoint("", omitHttps: false)); + } + + [Fact] + public void GetContentstackEndpoint_DictionaryOverload_UnknownRegion_ThrowsKeyNotFoundException() + { + Assert.Throws(() => + Endpoint.GetContentstackEndpoint("invalid-region-xyz")); + } + + // ── Cache consistency ───────────────────────────────────────────────── + + [Fact] + public void GetContentstackEndpoint_CalledTwice_ReturnsSameResult() + { + string url1 = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + string url2 = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + Assert.Equal(url1, url2); + } + + [Fact] + public void ResetCache_ThenResolve_StillReturnsCorrectUrl() + { + // Warm the cache + Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + + // Reset and resolve again + Endpoint.ResetCache(); + string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + Assert.Equal("https://cdn.contentstack.io", url); + } + + // ── Local file path ─────────────────────────────────────────────────── + + [Fact] + public void GetLocalFilePath_ContainsAssetsAndRegionsJson() + { + string path = Endpoint.GetLocalFilePath(); + Assert.Contains("Assets", path); + Assert.EndsWith("regions.json", path); + } + + [Fact] + public void GetLocalFilePath_PathIsAbsolute() + { + string path = Endpoint.GetLocalFilePath(); + Assert.True(Path.IsPathRooted(path), $"Expected absolute path, got: {path}"); + } + + // ── Local file self-heal ────────────────────────────────────────────── + + [Fact] + public void GetContentstackEndpoint_WritesLocalFile_AfterCdnDownload() + { + // Delete local file to force CDN download + string localFile = Endpoint.GetLocalFilePath(); + if (File.Exists(localFile)) + File.Delete(localFile); + + Endpoint.ResetCache(); + + // This call should trigger CDN download and write the file + Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + + Assert.True(File.Exists(localFile), $"Expected regions.json to be written to: {localFile}"); + } + + // ── URL format validation ───────────────────────────────────────────── + + [Theory] + [InlineData("na")] + [InlineData("eu")] + [InlineData("au")] + [InlineData("azure-na")] + [InlineData("azure-eu")] + [InlineData("gcp-na")] + [InlineData("gcp-eu")] + public void GetContentstackEndpoint_AllRegions_ContentDelivery_StartsWithHttps(string region) + { + string url = Endpoint.GetContentstackEndpoint(region, "contentDelivery"); + Assert.StartsWith("https://", url); + } + + [Theory] + [InlineData("na")] + [InlineData("eu")] + [InlineData("au")] + [InlineData("azure-na")] + [InlineData("azure-eu")] + [InlineData("gcp-na")] + [InlineData("gcp-eu")] + public void GetContentstackEndpoint_AllRegions_OmitHttps_DoesNotStartWithHttps(string region) + { + string host = Endpoint.GetContentstackEndpoint(region, "contentDelivery", omitHttps: true); + Assert.DoesNotMatch(@"^https?://", host); + } + } +} diff --git a/Contentstack.Core.Tests/Helpers/EntryFactory.cs b/Contentstack.Core.Tests/Helpers/EntryFactory.cs index 33e9d81a..d980d123 100644 --- a/Contentstack.Core.Tests/Helpers/EntryFactory.cs +++ b/Contentstack.Core.Tests/Helpers/EntryFactory.cs @@ -228,7 +228,7 @@ public async Task> FetchFirstEntryAsync(string con /// /// Content type UID /// Count result - public async Task CountEntriesAsync(string contentTypeUid) + public async Task CountEntriesAsync(string contentTypeUid) { return await CreateQuery(contentTypeUid).Count(); } diff --git a/Contentstack.Core.Tests/Integration/AssetTests/AssetManagementComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/AssetTests/AssetManagementComprehensiveTest.cs index 59c251dc..f70c1b0f 100644 --- a/Contentstack.Core.Tests/Integration/AssetTests/AssetManagementComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/AssetTests/AssetManagementComprehensiveTest.cs @@ -560,7 +560,7 @@ public async Task Asset_Tags_Available() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/BranchTests/MetadataBranchComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/BranchTests/MetadataBranchComprehensiveTest.cs index 9889aa01..ef815989 100644 --- a/Contentstack.Core.Tests/Integration/BranchTests/MetadataBranchComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/BranchTests/MetadataBranchComprehensiveTest.cs @@ -382,7 +382,7 @@ public async Task Metadata_Performance_WithOwner() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/CachingTests/CachePersistenceTest.cs b/Contentstack.Core.Tests/Integration/CachingTests/CachePersistenceTest.cs index a14dddbd..8675d6a9 100644 --- a/Contentstack.Core.Tests/Integration/CachingTests/CachePersistenceTest.cs +++ b/Contentstack.Core.Tests/Integration/CachingTests/CachePersistenceTest.cs @@ -439,7 +439,7 @@ public async Task Cache_DifferentProjections_IndependentCache() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/ClientTests/ContentstackClientTest.cs b/Contentstack.Core.Tests/Integration/ClientTests/ContentstackClientTest.cs index 25c510ff..2614e0a7 100644 --- a/Contentstack.Core.Tests/Integration/ClientTests/ContentstackClientTest.cs +++ b/Contentstack.Core.Tests/Integration/ClientTests/ContentstackClientTest.cs @@ -17,7 +17,7 @@ public ContentstackClientTest(ITestOutputHelper output) : base(output) { } - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/ConfigurationTests/RegionSupportTest.cs b/Contentstack.Core.Tests/Integration/ConfigurationTests/RegionSupportTest.cs index 166662b5..8e8e8182 100644 --- a/Contentstack.Core.Tests/Integration/ConfigurationTests/RegionSupportTest.cs +++ b/Contentstack.Core.Tests/Integration/ConfigurationTests/RegionSupportTest.cs @@ -296,13 +296,14 @@ public void Region_AllValues_AreDefined() LogArrange("Setting up test"); var regions = Enum.GetValues(); - TestAssert.Equal(6, regions.Length); + TestAssert.Equal(7, regions.Length); TestAssert.Contains(ContentstackRegion.US, regions); TestAssert.Contains(ContentstackRegion.EU, regions); TestAssert.Contains(ContentstackRegion.AZURE_EU, regions); TestAssert.Contains(ContentstackRegion.AZURE_NA, regions); TestAssert.Contains(ContentstackRegion.GCP_NA, regions); TestAssert.Contains(ContentstackRegion.AU, regions); + TestAssert.Contains(ContentstackRegion.GCP_EU, regions); } [Fact(DisplayName = "Region Configuration - Region Options Default Value Is US")] diff --git a/Contentstack.Core.Tests/Integration/ContentTypeTests/ContentTypeOperationsTest.cs b/Contentstack.Core.Tests/Integration/ContentTypeTests/ContentTypeOperationsTest.cs index 81b5c969..f6e290d9 100644 --- a/Contentstack.Core.Tests/Integration/ContentTypeTests/ContentTypeOperationsTest.cs +++ b/Contentstack.Core.Tests/Integration/ContentTypeTests/ContentTypeOperationsTest.cs @@ -6,7 +6,7 @@ using Contentstack.Core.Configuration; using Contentstack.Core.Models; using Contentstack.Core.Tests.Helpers; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; using System.Collections; using Xunit.Abstractions; @@ -165,10 +165,10 @@ public async Task ContentType_FetchSingleContentType_ReturnsSchema() LogAssert("Verifying response"); TestAssert.NotNull(schema); - TestAssert.IsType(schema); + TestAssert.IsType(schema); // Schema should contain uid TestAssert.True(schema.ContainsKey("uid")); - TestAssert.Equal(TestDataHelper.SimpleContentTypeUid, schema["uid"]?.ToString()); + TestAssert.Equal(TestDataHelper.SimpleContentTypeUid, schema["uid"]?.GetValue()); } [Fact(DisplayName = "Content Type - Content Type Fetch With Global Fields Includes Global Field Schema")] @@ -194,7 +194,7 @@ public async Task ContentType_FetchWithGlobalFields_IncludesGlobalFieldSchema() LogAssert("Verifying response"); TestAssert.NotNull(schema); - TestAssert.IsType(schema); + TestAssert.IsType(schema); } [Fact(DisplayName = "Content Type - Content Type Fetch Complex Type Contains Expected Fields")] @@ -216,7 +216,7 @@ public async Task ContentType_FetchComplexType_ContainsExpectedFields() LogAssert("Verifying response"); TestAssert.NotNull(schema); - TestAssert.IsType(schema); + TestAssert.IsType(schema); // Should have schema field TestAssert.True(schema.ContainsKey("schema")); } @@ -287,7 +287,7 @@ public async Task ContentType_Schema_ContainsUid() TestAssert.NotNull(schema); TestAssert.True(schema.ContainsKey("uid")); - TestAssert.Equal(TestDataHelper.MediumContentTypeUid, schema["uid"].ToString()); + TestAssert.Equal(TestDataHelper.MediumContentTypeUid, schema["uid"]?.GetValue()); } [Fact(DisplayName = "Content Type - Content Type Schema Contains Schema Definition")] @@ -310,7 +310,7 @@ public async Task ContentType_Schema_ContainsSchemaDefinition() TestAssert.NotNull(schema); TestAssert.True(schema.ContainsKey("schema")); - var schemaArray = schema["schema"] as JArray; + var schemaArray = schema["schema"] as JsonArray; TestAssert.NotNull(schemaArray); TestAssert.True(schemaArray.Count > 0, "Schema should contain field definitions"); } @@ -402,7 +402,7 @@ public async Task ContentType_MultipleContentTypes_AllFetchSuccessfully() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/ContentTypeTests/ContentTypeQueryTest.cs b/Contentstack.Core.Tests/Integration/ContentTypeTests/ContentTypeQueryTest.cs index eb3a629d..0955c235 100644 --- a/Contentstack.Core.Tests/Integration/ContentTypeTests/ContentTypeQueryTest.cs +++ b/Contentstack.Core.Tests/Integration/ContentTypeTests/ContentTypeQueryTest.cs @@ -6,7 +6,7 @@ using Contentstack.Core.Configuration; using Contentstack.Core.Models; using Contentstack.Core.Tests.Helpers; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; using Xunit.Abstractions; namespace Contentstack.Core.Tests.Integration.ContentTypeTests @@ -117,7 +117,7 @@ public async Task ContentTypeQuery_SchemaValidation_IsValidJObject() LogAssert("Verifying response"); TestAssert.NotNull(schema); - TestAssert.IsType(schema); + TestAssert.IsType(schema); } #endregion @@ -360,7 +360,7 @@ await TestAssert.ThrowsAnyAsync(async () => #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/EntryTests/EntryIncludeExtendedTest.cs b/Contentstack.Core.Tests/Integration/EntryTests/EntryIncludeExtendedTest.cs index 9d32523e..e9fbdf30 100644 --- a/Contentstack.Core.Tests/Integration/EntryTests/EntryIncludeExtendedTest.cs +++ b/Contentstack.Core.Tests/Integration/EntryTests/EntryIncludeExtendedTest.cs @@ -390,7 +390,7 @@ public async Task EntryInclude_Performance_MultipleIncludes() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/EntryTests/EntryOperationsComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/EntryTests/EntryOperationsComprehensiveTest.cs index 83732f74..5319b71c 100644 --- a/Contentstack.Core.Tests/Integration/EntryTests/EntryOperationsComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/EntryTests/EntryOperationsComprehensiveTest.cs @@ -734,7 +734,7 @@ public async Task Entry_AddParam_CustomParamIsApplied() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/EntryTests/FieldProjectionAndReferencesTest.cs b/Contentstack.Core.Tests/Integration/EntryTests/FieldProjectionAndReferencesTest.cs index cdd8d5cf..19fb2680 100644 --- a/Contentstack.Core.Tests/Integration/EntryTests/FieldProjectionAndReferencesTest.cs +++ b/Contentstack.Core.Tests/Integration/EntryTests/FieldProjectionAndReferencesTest.cs @@ -7,7 +7,6 @@ using Contentstack.Core.Models; using Contentstack.Core.Tests.Helpers; using Contentstack.Core.Tests.Models; -using Newtonsoft.Json.Linq; using Xunit.Abstractions; namespace Contentstack.Core.Tests.Integration.EntryTests @@ -619,7 +618,7 @@ public async Task ReferenceQuery_WithOwner_IncludesOwnerInformation() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/GlobalFieldsTests/GlobalFieldsComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/GlobalFieldsTests/GlobalFieldsComprehensiveTest.cs index 1e46fe18..44b18f5e 100644 --- a/Contentstack.Core.Tests/Integration/GlobalFieldsTests/GlobalFieldsComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/GlobalFieldsTests/GlobalFieldsComprehensiveTest.cs @@ -6,7 +6,7 @@ using Contentstack.Core.Configuration; using Contentstack.Core.Models; using Contentstack.Core.Tests.Helpers; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; using Xunit.Abstractions; namespace Contentstack.Core.Tests.Integration.GlobalFieldsTests @@ -412,7 +412,7 @@ public async Task GlobalFields_SchemaValidation_IsValidJObject() LogAssert("Verifying response"); TestAssert.NotNull(schema); - TestAssert.IsType(schema); + TestAssert.IsType(schema); } #endregion @@ -505,7 +505,7 @@ public async Task GlobalFields_EmptyGlobalField_ReturnsValidEntry() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/GlobalFieldsTests/NestedGlobalFieldsTest.cs b/Contentstack.Core.Tests/Integration/GlobalFieldsTests/NestedGlobalFieldsTest.cs index 52b31662..e02728c0 100644 --- a/Contentstack.Core.Tests/Integration/GlobalFieldsTests/NestedGlobalFieldsTest.cs +++ b/Contentstack.Core.Tests/Integration/GlobalFieldsTests/NestedGlobalFieldsTest.cs @@ -6,7 +6,7 @@ using Contentstack.Core.Configuration; using Contentstack.Core.Models; using Contentstack.Core.Tests.Helpers; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; using Xunit.Abstractions; namespace Contentstack.Core.Tests.Integration.GlobalFieldsTests @@ -386,7 +386,7 @@ public async Task NestedGlobal_SchemaValidation_IsValidJObject() LogAssert("Verifying response"); TestAssert.NotNull(schema); - TestAssert.IsType(schema); + TestAssert.IsType(schema); } #endregion @@ -425,7 +425,7 @@ public async Task NestedGlobal_Performance_DeepNesting() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/HeaderTests/HeaderManagementTest.cs b/Contentstack.Core.Tests/Integration/HeaderTests/HeaderManagementTest.cs index 741ab4ab..4044c937 100644 --- a/Contentstack.Core.Tests/Integration/HeaderTests/HeaderManagementTest.cs +++ b/Contentstack.Core.Tests/Integration/HeaderTests/HeaderManagementTest.cs @@ -343,7 +343,7 @@ public async Task Header_RequestLevel_IndependentRequests() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/ImageDeliveryTests/ImageDeliveryComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/ImageDeliveryTests/ImageDeliveryComprehensiveTest.cs index 5a477e52..17171699 100644 --- a/Contentstack.Core.Tests/Integration/ImageDeliveryTests/ImageDeliveryComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/ImageDeliveryTests/ImageDeliveryComprehensiveTest.cs @@ -311,7 +311,7 @@ public async Task ImageDelivery_Performance_MultipleAssets() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/JSONRTETests/JsonRteEmbeddedItemsTest.cs b/Contentstack.Core.Tests/Integration/JSONRTETests/JsonRteEmbeddedItemsTest.cs index 1b53f515..49af2d50 100644 --- a/Contentstack.Core.Tests/Integration/JSONRTETests/JsonRteEmbeddedItemsTest.cs +++ b/Contentstack.Core.Tests/Integration/JSONRTETests/JsonRteEmbeddedItemsTest.cs @@ -52,7 +52,7 @@ public async Task JsonRte_BasicFetch_ReturnsEntry() var blocks = entry.Get("json_rte_field"); if (blocks != null) { - var blocksArray = blocks as Newtonsoft.Json.Linq.JArray; + var blocksArray = blocks as System.Collections.IList; if (blocksArray != null && blocksArray.Count > 0) { TestAssert.True(blocksArray.Count > 0, "Modular blocks should have content"); @@ -90,7 +90,7 @@ public async Task JsonRte_WithEmbeddedItems_IncludesEmbedded() var blocks = entry.Get("json_rte_field"); if (blocks != null) { - var blocksArray = blocks as Newtonsoft.Json.Linq.JArray; + var blocksArray = blocks as System.Collections.IList; if (blocksArray != null && blocksArray.Count > 0) { TestAssert.True(blocksArray.Count > 0, "Modular blocks should have content"); @@ -132,7 +132,7 @@ public async Task JsonRte_EmbeddedEntry_SingleLevel() var blocks = entry.Get("json_rte_field"); if (blocks != null) { - var blocksArray = blocks as Newtonsoft.Json.Linq.JArray; + var blocksArray = blocks as System.Collections.IList; if (blocksArray != null && blocksArray.Count > 0) { TestAssert.True(blocksArray.Count > 0, "Modular blocks should have content"); @@ -426,7 +426,7 @@ public async Task JsonRte_EmptyRte_HandlesGracefully() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/LocalizationTests/LocaleFallbackChainTest.cs b/Contentstack.Core.Tests/Integration/LocalizationTests/LocaleFallbackChainTest.cs index 91e47d55..0ba86c67 100644 --- a/Contentstack.Core.Tests/Integration/LocalizationTests/LocaleFallbackChainTest.cs +++ b/Contentstack.Core.Tests/Integration/LocalizationTests/LocaleFallbackChainTest.cs @@ -399,7 +399,7 @@ public async Task Fallback_NoTranslation_FallsBackCompletely() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/LocalizationTests/LocalizationExtendedTest.cs b/Contentstack.Core.Tests/Integration/LocalizationTests/LocalizationExtendedTest.cs index 19505919..0bc6493d 100644 --- a/Contentstack.Core.Tests/Integration/LocalizationTests/LocalizationExtendedTest.cs +++ b/Contentstack.Core.Tests/Integration/LocalizationTests/LocalizationExtendedTest.cs @@ -257,7 +257,7 @@ public async Task LocaleExtended_MultipleLocaleRequests_Independent() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/ModularBlocksTests/ModularBlocksComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/ModularBlocksTests/ModularBlocksComprehensiveTest.cs index cdd275b7..1e553fd0 100644 --- a/Contentstack.Core.Tests/Integration/ModularBlocksTests/ModularBlocksComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/ModularBlocksTests/ModularBlocksComprehensiveTest.cs @@ -52,7 +52,7 @@ public async Task ModularBlocks_BasicFetch_ReturnsEntry() var blocks = entry.Get("modular_blocks"); if (blocks != null) { - var blocksArray = blocks as Newtonsoft.Json.Linq.JArray; + var blocksArray = blocks as System.Collections.IList; if (blocksArray != null && blocksArray.Count > 0) { TestAssert.True(blocksArray.Count > 0, "Modular blocks should have content"); @@ -418,7 +418,7 @@ public async Task ModularBlocks_EmptyBlocks_HandlesGracefully() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/PaginationTests/PaginationComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/PaginationTests/PaginationComprehensiveTest.cs index aac04730..201eb26a 100644 --- a/Contentstack.Core.Tests/Integration/PaginationTests/PaginationComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/PaginationTests/PaginationComprehensiveTest.cs @@ -282,7 +282,7 @@ public async Task Pagination_LargeSkip_HandlesGracefully() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/PerformanceTests/PerformanceLargeDatasetsTest.cs b/Contentstack.Core.Tests/Integration/PerformanceTests/PerformanceLargeDatasetsTest.cs index 970b47f9..1d501eae 100644 --- a/Contentstack.Core.Tests/Integration/PerformanceTests/PerformanceLargeDatasetsTest.cs +++ b/Contentstack.Core.Tests/Integration/PerformanceTests/PerformanceLargeDatasetsTest.cs @@ -440,7 +440,7 @@ public async Task Performance_CachedVsUncached_Consistency() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/QueryEncodingTests/QueryEncodingComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/QueryEncodingTests/QueryEncodingComprehensiveTest.cs index 58c9ed57..36b6cb4d 100644 --- a/Contentstack.Core.Tests/Integration/QueryEncodingTests/QueryEncodingComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/QueryEncodingTests/QueryEncodingComprehensiveTest.cs @@ -732,7 +732,7 @@ public async Task Encoding_MixedCharacterSet_AllTypesEncoded() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/QueryTests/AdvancedQueryFeaturesTest.cs b/Contentstack.Core.Tests/Integration/QueryTests/AdvancedQueryFeaturesTest.cs index 502607fc..50f40093 100644 --- a/Contentstack.Core.Tests/Integration/QueryTests/AdvancedQueryFeaturesTest.cs +++ b/Contentstack.Core.Tests/Integration/QueryTests/AdvancedQueryFeaturesTest.cs @@ -680,7 +680,7 @@ public async Task AdvancedQuery_QueryResultStructure_IsValid() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/QueryTests/ComplexFieldQueriesTest.cs b/Contentstack.Core.Tests/Integration/QueryTests/ComplexFieldQueriesTest.cs index 7a51b4a6..9c6ce8d6 100644 --- a/Contentstack.Core.Tests/Integration/QueryTests/ComplexFieldQueriesTest.cs +++ b/Contentstack.Core.Tests/Integration/QueryTests/ComplexFieldQueriesTest.cs @@ -371,7 +371,7 @@ public async Task ComplexField_Performance_DeepNestedQuery() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/QueryTests/ComplexQueryCombinationsTest.cs b/Contentstack.Core.Tests/Integration/QueryTests/ComplexQueryCombinationsTest.cs index c84c8745..de395339 100644 --- a/Contentstack.Core.Tests/Integration/QueryTests/ComplexQueryCombinationsTest.cs +++ b/Contentstack.Core.Tests/Integration/QueryTests/ComplexQueryCombinationsTest.cs @@ -337,7 +337,7 @@ public async Task ComplexQuery_Performance_NestedCombinations() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/QueryTests/EntryQueryablesComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/QueryTests/EntryQueryablesComprehensiveTest.cs index e7c88f0a..cba8db06 100644 --- a/Contentstack.Core.Tests/Integration/QueryTests/EntryQueryablesComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/QueryTests/EntryQueryablesComprehensiveTest.cs @@ -799,7 +799,7 @@ public async Task Query_Count_ReturnsCorrectCount() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/QueryTests/QueryIncludeExtendedTest.cs b/Contentstack.Core.Tests/Integration/QueryTests/QueryIncludeExtendedTest.cs index 53768c40..fcb5485f 100644 --- a/Contentstack.Core.Tests/Integration/QueryTests/QueryIncludeExtendedTest.cs +++ b/Contentstack.Core.Tests/Integration/QueryTests/QueryIncludeExtendedTest.cs @@ -397,7 +397,7 @@ public async Task QueryInclude_Performance_MultipleIncludes() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/QueryTests/QueryOperatorsComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/QueryTests/QueryOperatorsComprehensiveTest.cs index ee10f97e..ed630bc1 100644 --- a/Contentstack.Core.Tests/Integration/QueryTests/QueryOperatorsComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/QueryTests/QueryOperatorsComprehensiveTest.cs @@ -840,7 +840,7 @@ public async Task Query_ChainedOperations_ExecutesInOrder() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/ReferenceTests/DeepReferencesComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/ReferenceTests/DeepReferencesComprehensiveTest.cs index 82320426..112ea91f 100644 --- a/Contentstack.Core.Tests/Integration/ReferenceTests/DeepReferencesComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/ReferenceTests/DeepReferencesComprehensiveTest.cs @@ -6,7 +6,6 @@ using Contentstack.Core.Configuration; using Contentstack.Core.Models; using Contentstack.Core.Tests.Helpers; -using Newtonsoft.Json.Linq; using Xunit.Abstractions; namespace Contentstack.Core.Tests.Integration.ReferenceTests @@ -502,7 +501,7 @@ public async Task DeepRef_ReferenceContentTypeUID_IncludesContentTypeInfo() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/ReferenceTests/MultiReferenceTest.cs b/Contentstack.Core.Tests/Integration/ReferenceTests/MultiReferenceTest.cs index 389c40a7..02fc11f6 100644 --- a/Contentstack.Core.Tests/Integration/ReferenceTests/MultiReferenceTest.cs +++ b/Contentstack.Core.Tests/Integration/ReferenceTests/MultiReferenceTest.cs @@ -429,7 +429,7 @@ public async Task MultiRef_EmptyReferenceArray_HandlesGracefully() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/RetryTests/RetryIntegrationTest.cs b/Contentstack.Core.Tests/Integration/RetryTests/RetryIntegrationTest.cs index 12d8145d..6ad765eb 100644 --- a/Contentstack.Core.Tests/Integration/RetryTests/RetryIntegrationTest.cs +++ b/Contentstack.Core.Tests/Integration/RetryTests/RetryIntegrationTest.cs @@ -69,13 +69,17 @@ public async Task Retry_MultipleSuccessfulRequests_Consistent() var task3 = client.ContentType(TestDataHelper.ComplexContentTypeUid).Entry(TestDataHelper.ComplexEntryUid).Fetch(); await Task.WhenAll(task1, task2, task3); - + + var entry1 = await task1; + var entry2 = await task2; + var entry3 = await task3; + // Assert LogAssert("Verifying response"); - TestAssert.NotNull(task1.Result); - TestAssert.NotNull(task2.Result); - TestAssert.NotNull(task3.Result); + TestAssert.NotNull(entry1); + TestAssert.NotNull(entry2); + TestAssert.NotNull(entry3); } #endregion @@ -155,19 +159,19 @@ public async Task Retry_ParallelRequests_HandlesLoad() .Fetch()); } - await Task.WhenAll(tasks); - + var entries = await Task.WhenAll(tasks); + // Assert - All should succeed LogAssert("Verifying response"); - TestAssert.True(tasks.All(t => t.Result != null)); + TestAssert.True(entries.All(e => e != null)); } #endregion #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/StackTests/StackOperationsComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/StackTests/StackOperationsComprehensiveTest.cs index 8d0e962c..9908926b 100644 --- a/Contentstack.Core.Tests/Integration/StackTests/StackOperationsComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/StackTests/StackOperationsComprehensiveTest.cs @@ -593,7 +593,7 @@ public async Task Stack_ConcurrentRequests_HandledCorrectly() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/SyncTests/ExtendedSyncApiTest.cs b/Contentstack.Core.Tests/Integration/SyncTests/ExtendedSyncApiTest.cs index bba47f31..2f18f3aa 100644 --- a/Contentstack.Core.Tests/Integration/SyncTests/ExtendedSyncApiTest.cs +++ b/Contentstack.Core.Tests/Integration/SyncTests/ExtendedSyncApiTest.cs @@ -572,7 +572,7 @@ public async Task ExtendedSync_DateBasedFlow_RecentChanges() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/SyncTests/SyncApiComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/SyncTests/SyncApiComprehensiveTest.cs index de7194df..e6a2181c 100644 --- a/Contentstack.Core.Tests/Integration/SyncTests/SyncApiComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/SyncTests/SyncApiComprehensiveTest.cs @@ -402,7 +402,7 @@ await TestAssert.ThrowsAnyAsync(async () => #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomySupportTest.cs b/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomySupportTest.cs index e5846c44..4b65e33e 100644 --- a/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomySupportTest.cs +++ b/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomySupportTest.cs @@ -910,7 +910,7 @@ public async Task Taxonomy_WithEmptyOrNullStream_HandlesGracefully() #region Helper Methods - private ContentstackClient CreateClient() + private new ContentstackClient CreateClient() { var options = new ContentstackOptions() { diff --git a/Contentstack.Core.Tests/Mocks/EntryTestHelper.cs b/Contentstack.Core.Tests/Mocks/EntryTestHelper.cs index 41b50c92..4252c6c7 100644 --- a/Contentstack.Core.Tests/Mocks/EntryTestHelper.cs +++ b/Contentstack.Core.Tests/Mocks/EntryTestHelper.cs @@ -7,7 +7,7 @@ using Contentstack.Core.Models; using Contentstack.Core.Tests.Mocks; using Microsoft.Extensions.Options; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; namespace Contentstack.Core.Tests.Mocks { @@ -21,8 +21,8 @@ public static class EntryTestHelper /// public static Entry CreateEntryFromJson(string jsonString, ContentstackClient client, string contentTypeId = "source") { - JObject jsonObj = JObject.Parse(jsonString); - JToken entryToken = jsonObj["entry"] ?? jsonObj; + JsonObject jsonObj = JsonNode.Parse(jsonString).AsObject(); + JsonNode entryToken = jsonObj["entry"] ?? jsonObj; // Create Entry using internal constructor var entry = CreateEntryInternal(client, contentTypeId); @@ -30,7 +30,7 @@ public static Entry CreateEntryFromJson(string jsonString, ContentstackClient cl // Use reflection to call ParseObject var parseMethod = typeof(Entry).GetMethod("ParseObject", BindingFlags.NonPublic | BindingFlags.Instance); - parseMethod?.Invoke(entry, new object[] { entryToken as JObject, null }); + parseMethod?.Invoke(entry, new object[] { entryToken.AsObject(), null }); return entry; } diff --git a/Contentstack.Core.Tests/Mocks/MockResponse.cs b/Contentstack.Core.Tests/Mocks/MockResponse.cs index e14d4283..ccef3611 100644 --- a/Contentstack.Core.Tests/Mocks/MockResponse.cs +++ b/Contentstack.Core.Tests/Mocks/MockResponse.cs @@ -1,7 +1,7 @@ using System; using System.IO; using System.Reflection; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; namespace Contentstack.Core.Tests.Mocks { @@ -59,24 +59,24 @@ public static string CreateContentstackResponse(string fileName) } /// - /// Creates a JObject from a mock response file + /// Creates a from a mock response file /// /// Name of the JSON file - /// JObject parsed from the response - public static JObject CreateContentstackResponseAsJObject(string fileName) + /// JSON object parsed from the response + public static JsonObject CreateContentstackResponseAsJsonObject(string fileName) { var jsonString = CreateContentstackResponse(fileName); - return JObject.Parse(jsonString); + return JsonNode.Parse(jsonString).AsObject(); } /// - /// Creates a mock response from a JObject + /// Creates a mock response from a /// - /// The JObject to serialize + /// The JSON object to serialize /// JSON string response - public static string CreateContentstackResponseFromJObject(JObject jObject) + public static string CreateContentstackResponseFromJsonObject(JsonObject jsonObject) { - return jObject.ToString(); + return jsonObject.ToJsonString(); } } } diff --git a/Contentstack.Core.Tests/Models/SourceModel.cs b/Contentstack.Core.Tests/Models/SourceModel.cs index cc10c612..4624258a 100644 --- a/Contentstack.Core.Tests/Models/SourceModel.cs +++ b/Contentstack.Core.Tests/Models/SourceModel.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; using Contentstack.Core.Models; using Markdig; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace Contentstack.Core.Tests.Models { @@ -19,8 +20,6 @@ public class SourceModel public Asset file; public List Reference; public List Other_reference; - // public List> Reference; - // public List> Other_reference; public Dictionary Group; public List> Modular_blocks; public object[] Tags; @@ -29,6 +28,7 @@ public class SourceModel [JsonConverter(typeof(CustomDateTimeConverter))] public DateTime updated_at; public string Updated_by; + public String GetHTMLText() { string result = string.Empty; @@ -45,14 +45,25 @@ public String GetHTMLText() } return result; } - } - public class CustomDateTimeConverter : IsoDateTimeConverter + public class CustomDateTimeConverter : JsonConverter { - public CustomDateTimeConverter() + private const string Format = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var s = reader.GetString(); + if (string.IsNullOrEmpty(s)) + { + return default; + } + return DateTime.ParseExact(s, Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { - base.DateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + writer.WriteStringValue(value.ToUniversalTime().ToString(Format, CultureInfo.InvariantCulture)); } } } diff --git a/Contentstack.Core.Tests/Models/TestPlugin.cs b/Contentstack.Core.Tests/Models/TestPlugin.cs index 3703723a..97cef0c0 100644 --- a/Contentstack.Core.Tests/Models/TestPlugin.cs +++ b/Contentstack.Core.Tests/Models/TestPlugin.cs @@ -2,25 +2,27 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; +using Contentstack.Core.Internals; using Contentstack.Core.Interfaces; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Contentstack.Core.Tests.Models { - public class TestPlugin: IContentstackPlugin + public class TestPlugin : IContentstackPlugin { private ContentstackClient Client; - private JObject injectData; - - private string resp = $"{{\"emails\":[{string.Join(",", new List(){$"\"test\""})}]}}"; + private JsonObject injectData; + + private string resp = $"{{\"emails\":[{string.Join(",", new List() { $"\"test\"" })}]}}"; + public TestPlugin(ContentstackClient client) { Client = client; - injectData = JsonConvert.DeserializeObject(resp.Replace("\r\n", ""), Client.SerializerSettings); + injectData = JsonNode.Parse(resp.Replace("\r\n", "")).AsObject(); } - + public virtual async Task OnRequest(ContentstackClient stack, HttpWebRequest request) { request.Headers["test-header"] = "new header"; @@ -30,49 +32,47 @@ public virtual async Task OnRequest(ContentstackClient stack, Ht public virtual async Task OnResponse(ContentstackClient stack, HttpWebRequest request, HttpWebResponse response, string responseString) { - JObject data = JsonConvert.DeserializeObject(responseString.Replace("\r\n", ""), Client.SerializerSettings); + JsonObject data = JsonNode.Parse(responseString.Replace("\r\n", "")).AsObject(); _ = await Client.AssetLibrary().FetchAll(); updateLivePreviewContent(data); - return JsonConvert.SerializeObject(data); ; + return JsonSerializer.Serialize(data, Client.SerializerOptions); } - internal void updateLivePreviewContent(JObject response) + internal void updateLivePreviewContent(JsonObject response) { if (response.ContainsKey("uid")) { - response.Merge(injectData, new JsonMergeSettings() - { - MergeArrayHandling = MergeArrayHandling.Replace - }); + JsonObjectMerge.UnionMergeInto(response, injectData); + return; } - else + + foreach (var key in response.Select(p => p.Key).ToList()) { - foreach (var content in response) + var node = response[key]; + if (node is JsonArray arr) + { + updateArray(arr); + } + else if (node is JsonObject jo) { - if (content.Value.Type == JTokenType.Array) - { - updateArray((JArray)response[content.Key]); - } - else if (content.Value.Type == JTokenType.Object) - { - updateLivePreviewContent((JObject)response[content.Key]); - } + updateLivePreviewContent(jo); } } } - internal void updateArray(JArray array) + internal void updateArray(JsonArray array) { - for (var i = 0; i < array.Count(); i++) + for (var i = 0; i < array.Count; i++) { - if (array[i].Type == JTokenType.Array) + var item = array[i]; + if (item is JsonArray childArr) { - updateArray((JArray)array[i]); + updateArray(childArr); } - else if (array[i].Type == JTokenType.Object) + else if (item is JsonObject jo) { - updateLivePreviewContent((JObject)array[i]); + updateLivePreviewContent(jo); } } } diff --git a/Contentstack.Core.Tests/StackConfig.cs b/Contentstack.Core.Tests/StackConfig.cs index fe9a39de..68382242 100644 --- a/Contentstack.Core.Tests/StackConfig.cs +++ b/Contentstack.Core.Tests/StackConfig.cs @@ -13,7 +13,13 @@ System.Configuration.Configuration assemblyConfiguration { get { - return ConfigurationManager.OpenExeConfiguration(new Uri(uriString: GetType().Assembly.CodeBase).LocalPath); + var assembly = GetType().Assembly; + var path = assembly.Location; + if (string.IsNullOrEmpty(path)) + { + throw new InvalidOperationException("Assembly.Location is unavailable; cannot load assembly configuration."); + } + return ConfigurationManager.OpenExeConfiguration(path); } } diff --git a/Contentstack.Core.Unit.Tests/AssetLibraryUnitTests.cs b/Contentstack.Core.Unit.Tests/AssetLibraryUnitTests.cs index e819c735..18c590c8 100644 --- a/Contentstack.Core.Unit.Tests/AssetLibraryUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/AssetLibraryUnitTests.cs @@ -7,7 +7,7 @@ using Contentstack.Core.Internals; using Contentstack.Core.Models; using Microsoft.Extensions.Options; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; using Xunit; namespace Contentstack.Core.Unit.Tests @@ -63,9 +63,9 @@ public void Where_AddsQueryParameter() var urlQueries = (Dictionary)urlQueriesField?.GetValue(assetLibrary); Assert.True(urlQueries?.ContainsKey("query") ?? false); - var query = urlQueries?["query"] as JObject; + var query = urlQueries?["query"] as JsonObject; Assert.NotNull(query); - Assert.Equal(value, query?[key]?.ToString()); + Assert.Equal(value, query[key]?.GetValue()); } [Fact] @@ -115,8 +115,8 @@ public void Where_WithExistingKey_UpdatesQueryParameter() var urlQueriesField = typeof(AssetLibrary).GetField("UrlQueries", BindingFlags.NonPublic | BindingFlags.Instance); var urlQueries = (Dictionary)urlQueriesField?.GetValue(assetLibrary); - var query = urlQueries?["query"] as JObject; - Assert.Equal(value2, query?[key]?.ToString()); + var query = urlQueries?["query"] as JsonObject; + Assert.Equal(value2, query[key]?.GetValue()); } #endregion @@ -622,13 +622,13 @@ public void IncludeCount_AddsQueryParameter() #region Query Factory Method Tests [Fact] - public void Query_WithJObject_AddsQueryToUrlQueries() + public void Query_WithJsonObject_AddsQueryToUrlQueries() { // Arrange var assetLibrary = CreateAssetLibrary(); - var queryObject = new Newtonsoft.Json.Linq.JObject + var queryObject = new JsonObject { - { "field", "value" } + ["field"] = "value" }; // Act diff --git a/Contentstack.Core.Unit.Tests/ConfigUnitTests.cs b/Contentstack.Core.Unit.Tests/ConfigUnitTests.cs index fe0b332b..075f65a8 100644 --- a/Contentstack.Core.Unit.Tests/ConfigUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/ConfigUnitTests.cs @@ -84,9 +84,8 @@ public void Host_Get_ReturnsHostURL() // Act var result = config.Host; - // Assert - Assert.NotNull(result); - Assert.Contains("cdn.contentstack", result); + // Assert — Host is a custom override; defaults to empty string. URL resolution happens via BaseUrl. + Assert.Equal(string.Empty, result); } [Fact] @@ -274,12 +273,11 @@ public void HostURL_WithUSRegion_ReturnsIO() var config = CreateConfig(); config.Region = ContentstackRegion.US; - // Act - var hostUrlProperty = typeof(Config).GetProperty("HostURL", BindingFlags.NonPublic | BindingFlags.Instance); - var result = hostUrlProperty?.GetValue(config) as string; + // Act — HostURL was removed; BaseUrl is the public observable equivalent + var result = config.BaseUrl; // Assert - Assert.Equal("cdn.contentstack.io", result); + Assert.Contains("cdn.contentstack.io", result); } [Fact] @@ -290,11 +288,10 @@ public void HostURL_WithEURegion_ReturnsCOM() config.Region = ContentstackRegion.EU; // Act - var hostUrlProperty = typeof(Config).GetProperty("HostURL", BindingFlags.NonPublic | BindingFlags.Instance); - var result = hostUrlProperty?.GetValue(config) as string; + var result = config.BaseUrl; // Assert - Assert.Equal("cdn.contentstack.com", result); + Assert.Contains("eu-cdn.contentstack.com", result); } [Fact] @@ -305,11 +302,10 @@ public void HostURL_WithAzureEURegion_ReturnsCOM() config.Region = ContentstackRegion.AZURE_EU; // Act - var hostUrlProperty = typeof(Config).GetProperty("HostURL", BindingFlags.NonPublic | BindingFlags.Instance); - var result = hostUrlProperty?.GetValue(config) as string; + var result = config.BaseUrl; // Assert - Assert.Equal("cdn.contentstack.com", result); + Assert.Contains("azure-eu-cdn.contentstack.com", result); } [Fact] @@ -320,11 +316,10 @@ public void HostURL_WithAzureNARegion_ReturnsCOM() config.Region = ContentstackRegion.AZURE_NA; // Act - var hostUrlProperty = typeof(Config).GetProperty("HostURL", BindingFlags.NonPublic | BindingFlags.Instance); - var result = hostUrlProperty?.GetValue(config) as string; + var result = config.BaseUrl; // Assert - Assert.Equal("cdn.contentstack.com", result); + Assert.Contains("azure-na-cdn.contentstack.com", result); } [Fact] @@ -335,11 +330,10 @@ public void HostURL_WithGCPNARegion_ReturnsCOM() config.Region = ContentstackRegion.GCP_NA; // Act - var hostUrlProperty = typeof(Config).GetProperty("HostURL", BindingFlags.NonPublic | BindingFlags.Instance); - var result = hostUrlProperty?.GetValue(config) as string; + var result = config.BaseUrl; // Assert - Assert.Equal("cdn.contentstack.com", result); + Assert.Contains("gcp-na-cdn.contentstack.com", result); } [Fact] @@ -350,11 +344,10 @@ public void HostURL_WithAURegion_ReturnsCOM() config.Region = ContentstackRegion.AU; // Act - var hostUrlProperty = typeof(Config).GetProperty("HostURL", BindingFlags.NonPublic | BindingFlags.Instance); - var result = hostUrlProperty?.GetValue(config) as string; + var result = config.BaseUrl; // Assert - Assert.Equal("cdn.contentstack.com", result); + Assert.Contains("au-cdn.contentstack.com", result); } [Fact] @@ -364,12 +357,12 @@ public void RegionCode_WithUSRegion_ReturnsEmpty() var config = CreateConfig(); config.Region = ContentstackRegion.US; - // Act - var regionCodeMethod = typeof(Config).GetMethod("regionCode", BindingFlags.NonPublic | BindingFlags.Instance); - var result = regionCodeMethod?.Invoke(config, null) as string; + // Act — regionCode() was removed; US resolves to cdn.contentstack.io with no region prefix in the host + var result = config.BaseUrl; // Assert - Assert.Equal(string.Empty, result); + Assert.Contains("cdn.contentstack.io", result); + Assert.DoesNotContain("-cdn", result); } [Fact] @@ -379,13 +372,12 @@ public void RegionCode_WithEURegion_ReturnsFormattedCode() var config = CreateConfig(); config.Region = ContentstackRegion.EU; - // Act - var regionCodeMethod = typeof(Config).GetMethod("regionCode", BindingFlags.NonPublic | BindingFlags.Instance); - var result = regionCodeMethod?.Invoke(config, null) as string; + // Act — non-US regions resolve to a host with a region prefix (e.g. eu-cdn) + var result = config.BaseUrl; // Assert Assert.NotNull(result); - Assert.Contains("-", result); + Assert.Contains("-cdn", result); } [Fact] diff --git a/Contentstack.Core.Unit.Tests/Contentstack.Core.Unit.Tests.csproj b/Contentstack.Core.Unit.Tests/Contentstack.Core.Unit.Tests.csproj index a4138b9d..cfbb4691 100644 --- a/Contentstack.Core.Unit.Tests/Contentstack.Core.Unit.Tests.csproj +++ b/Contentstack.Core.Unit.Tests/Contentstack.Core.Unit.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net10.0 false $(Version) @@ -18,12 +18,9 @@ - - - diff --git a/Contentstack.Core.Unit.Tests/ContentstackClientForkTests.cs b/Contentstack.Core.Unit.Tests/ContentstackClientForkTests.cs deleted file mode 100644 index 6dd85446..00000000 --- a/Contentstack.Core.Unit.Tests/ContentstackClientForkTests.cs +++ /dev/null @@ -1,370 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using AutoFixture; -using Contentstack.Core.Configuration; -using Contentstack.Core.Unit.Tests.Helpers; -using Contentstack.Core.Unit.Tests.Mokes; -using Xunit; - -namespace Contentstack.Core.Unit.Tests -{ - /// - /// Unit tests for ContentstackClient.Fork() method - /// Tests client forking behavior, isolation, and configuration preservation - /// - [Trait("Category", "TimelinePreview")] - [Trait("Category", "Fork")] - public class ContentstackClientForkTests : ContentstackClientTestBase - { - #region Positive Test Cases - - [Fact] - public void Fork_CreatesIndependentClientInstance() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - - // Act - var forkedClient = parentClient.Fork(); - - // Assert - Assert.NotNull(forkedClient); - AssertClientsAreIndependent(parentClient, forkedClient); - } - - [Fact] - public void Fork_PreservesBaseConfiguration() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - - // Act - var forkedClient = parentClient.Fork(); - - // Assert - AssertConfigurationPreserved(parentClient, forkedClient); - } - - [Fact] - public void Fork_PreservesCustomHeaders() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var customHeaderKey = "X-Custom-Header"; - var customHeaderValue = _fixture.Create(); - - parentClient.SetHeader(customHeaderKey, customHeaderValue); - - // Act - var forkedClient = parentClient.Fork(); - - // Assert - // Verify custom header is preserved in forked client - var parentHeaders = GetInternalField>(parentClient, "_LocalHeaders"); - var forkedHeaders = GetInternalField>(forkedClient, "_LocalHeaders"); - - Assert.True(parentHeaders.ContainsKey(customHeaderKey)); - Assert.True(forkedHeaders.ContainsKey(customHeaderKey)); - Assert.Equal(parentHeaders[customHeaderKey], forkedHeaders[customHeaderKey]); - } - - [Fact] - public void Fork_CopiesContentTypeUidHints() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var contentTypeUid = _fixture.Create(); - var entryUid = _fixture.Create(); - - // Set content type and entry hints on parent - SetInternalField(parentClient, "currentContenttypeUid", contentTypeUid); - SetInternalField(parentClient, "currentEntryUid", entryUid); - - // Act - var forkedClient = parentClient.Fork(); - - // Assert - var forkedContentTypeUid = GetInternalField(forkedClient, "currentContenttypeUid"); - var forkedEntryUid = GetInternalField(forkedClient, "currentEntryUid"); - - Assert.Equal(contentTypeUid, forkedContentTypeUid); - Assert.Equal(entryUid, forkedEntryUid); - } - - [Fact] - public void Fork_IndependentLivePreviewConfig() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var parentConfig = parentClient.GetLivePreviewConfig(); - parentConfig.PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - - // Act - var forkedClient = parentClient.Fork(); - var forkedConfig = forkedClient.GetLivePreviewConfig(); - - // Assert - Configs are independent instances - Assert.NotSame(parentConfig, forkedConfig); - - // Assert - Initial values are copied - Assert.Equal(parentConfig.PreviewTimestamp, forkedConfig.PreviewTimestamp); - Assert.Equal(parentConfig.ReleaseId, forkedConfig.ReleaseId); - Assert.Equal(parentConfig.Enable, forkedConfig.Enable); - Assert.Equal(parentConfig.Host, forkedConfig.Host); - Assert.Equal(parentConfig.ManagementToken, forkedConfig.ManagementToken); - } - - [Fact] - public void Fork_SharedConfigurationReference() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var parentConfig = parentClient.GetLivePreviewConfig(); - parentConfig.PreviewResponse = CreateMockPreviewResponse(); - - // Act - var forkedClient = parentClient.Fork(); - var forkedConfig = forkedClient.GetLivePreviewConfig(); - - // Assert - PreviewResponse is shared reference (memory efficient) - Assert.Same(parentConfig.PreviewResponse, forkedConfig.PreviewResponse); - } - - [Fact] - public void Fork_MultipleLevels_IndependentContexts() - { - // Arrange - var level1Client = CreateClientWithTimeline(); - level1Client.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T10:00:00.000Z"; - - // Act - var level2Client = level1Client.Fork(); - level2Client.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T12:00:00.000Z"; - - var level3Client = level2Client.Fork(); - level3Client.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T14:00:00.000Z"; - - // Assert - Each level maintains its own timeline - Assert.Equal("2024-11-29T10:00:00.000Z", level1Client.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("2024-11-29T12:00:00.000Z", level2Client.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("2024-11-29T14:00:00.000Z", level3Client.GetLivePreviewConfig().PreviewTimestamp); - - // Assert - All are independent instances - AssertClientsAreIndependent(level1Client, level2Client); - AssertClientsAreIndependent(level2Client, level3Client); - AssertClientsAreIndependent(level1Client, level3Client); - } - - [Fact] - public void Fork_ParallelModifications_IsolatedChanges() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var fork1 = parentClient.Fork(); - var fork2 = parentClient.Fork(); - - // Act - Modify each fork independently - fork1.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T08:00:00.000Z"; - fork1.GetLivePreviewConfig().ReleaseId = "fork1_release"; - - fork2.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T16:00:00.000Z"; - fork2.GetLivePreviewConfig().ReleaseId = "fork2_release"; - - // Assert - Changes are isolated - Assert.Equal("2024-11-29T08:00:00.000Z", fork1.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("fork1_release", fork1.GetLivePreviewConfig().ReleaseId); - - Assert.Equal("2024-11-29T16:00:00.000Z", fork2.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("fork2_release", fork2.GetLivePreviewConfig().ReleaseId); - - // Parent client should maintain its original state - Assert.Equal("2024-11-29T14:30:00.000Z", parentClient.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("test_release_123", parentClient.GetLivePreviewConfig().ReleaseId); - } - - [Fact] - public void Fork_PreservesPlugins() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var mockHandler = new TimelineMockHttpHandler().ForSuccessfulLivePreview(); - parentClient.Plugins.Add(mockHandler); - - // Act - var forkedClient = parentClient.Fork(); - - // Assert - Plugins collection exists but fork doesn't share plugin instances - Assert.NotNull(forkedClient.Plugins); - Assert.Empty(forkedClient.Plugins); // Fork starts with empty plugins (isolated) - } - - [Fact] - public async Task Fork_IndependentLivePreviewOperations() - { - // Arrange - var parentClient = CreateClientWithLivePreview(); - var fork1 = parentClient.Fork(); - var fork2 = parentClient.Fork(); - - // Set up different timeline contexts - var query1 = CreateLivePreviewQuery(previewTimestamp: "2024-11-29T08:00:00.000Z"); - var query2 = CreateLivePreviewQuery(previewTimestamp: "2024-11-29T16:00:00.000Z"); - - // Act - await fork1.LivePreviewQueryAsync(query1); - await fork2.LivePreviewQueryAsync(query2); - - // Assert - Each fork maintains its own timeline context - Assert.Equal("2024-11-29T08:00:00.000Z", fork1.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("2024-11-29T16:00:00.000Z", fork2.GetLivePreviewConfig().PreviewTimestamp); - - // Parent should be unaffected - Assert.Null(parentClient.GetLivePreviewConfig().PreviewTimestamp); - } - - #endregion - - #region Negative Test Cases - - [Fact] - public void Fork_WithNullLivePreviewConfig_HandlesGracefully() - { - // Arrange - var parentClient = CreateClient(); // Client without LivePreview - SetInternalProperty(parentClient, "LivePreviewConfig", null); - - // Act & Assert - Should not throw - var forkedClient = parentClient.Fork(); - - Assert.NotNull(forkedClient); - // Verify the fork handles null config appropriately - var forkedConfig = forkedClient.GetLivePreviewConfig(); - Assert.NotNull(forkedConfig); // Should create a new config if parent was null - } - - [Fact] - public void Fork_WithCorruptedConfiguration_CreatesValidFork() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var config = parentClient.GetLivePreviewConfig(); - - // Corrupt some configuration properties - config.Host = null; - config.PreviewTimestamp = "invalid-timestamp-format"; - - // Act & Assert - Should not throw - var forkedClient = parentClient.Fork(); - var forkedConfig = forkedClient.GetLivePreviewConfig(); - - Assert.NotNull(forkedClient); - Assert.NotNull(forkedConfig); - Assert.Equal("invalid-timestamp-format", forkedConfig.PreviewTimestamp); // Corruption is copied but doesn't break fork - } - - [Fact] - public void Fork_AfterParentModification_IsolatesChanges() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var originalTimestamp = parentClient.GetLivePreviewConfig().PreviewTimestamp; - - // Act - Create fork, then modify parent - var forkedClient = parentClient.Fork(); - parentClient.GetLivePreviewConfig().PreviewTimestamp = "2024-12-01T00:00:00.000Z"; - - // Assert - Fork maintains original timestamp - Assert.Equal(originalTimestamp, forkedClient.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("2024-12-01T00:00:00.000Z", parentClient.GetLivePreviewConfig().PreviewTimestamp); - } - - [Fact] - public void Fork_WithLargeNumberOfForks_MaintainsPerformance() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var numberOfForks = 1000; - var forks = new ContentstackClient[numberOfForks]; - - // Act - Measure fork creation time - var startTime = DateTime.UtcNow; - - for (int i = 0; i < numberOfForks; i++) - { - forks[i] = parentClient.Fork(); - forks[i].GetLivePreviewConfig().PreviewTimestamp = $"2024-11-{(i % 12) + 1:D2}-01T00:00:00.000Z"; - } - - var duration = DateTime.UtcNow - startTime; - - // Assert - Fork creation should be fast (under 1 second for 1000 forks) - Assert.True(duration.TotalSeconds < 1.0, $"Fork creation took {duration.TotalMilliseconds}ms for {numberOfForks} forks"); - - // Verify all forks are independent - for (int i = 0; i < Math.Min(10, numberOfForks); i++) // Check first 10 for performance - { - AssertClientsAreIndependent(parentClient, forks[i]); - } - } - - #endregion - - #region Edge Cases - - [Fact] - public void Fork_WithEmptyHeaders_HandlesCorrectly() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - SetInternalField(parentClient, "_LocalHeaders", new Dictionary()); - - // Act - var forkedClient = parentClient.Fork(); - - // Assert - Assert.NotNull(forkedClient); - var forkedHeaders = GetInternalField>(forkedClient, "_LocalHeaders"); - Assert.NotNull(forkedHeaders); - } - - [Fact] - public void Fork_WithNullHeaders_HandlesCorrectly() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - SetInternalField(parentClient, "_LocalHeaders", null); - - // Act & Assert - Should not throw - var forkedClient = parentClient.Fork(); - Assert.NotNull(forkedClient); - } - - [Fact] - public void Fork_RecursiveForkModification_MaintainsIsolation() - { - // Arrange - var level1 = CreateClientWithTimeline(); - level1.GetLivePreviewConfig().ReleaseId = "level1_release"; - - var level2 = level1.Fork(); - level2.GetLivePreviewConfig().ReleaseId = "level2_release"; - - var level3 = level2.Fork(); - level3.GetLivePreviewConfig().ReleaseId = "level3_release"; - - // Act - Modify level2 after level3 is created - level2.GetLivePreviewConfig().PreviewTimestamp = "2024-11-30T00:00:00.000Z"; - - // Assert - Level3 should not be affected by level2 changes - Assert.Equal("level1_release", level1.GetLivePreviewConfig().ReleaseId); - Assert.Equal("level2_release", level2.GetLivePreviewConfig().ReleaseId); - Assert.Equal("level3_release", level3.GetLivePreviewConfig().ReleaseId); - - Assert.Equal("2024-11-30T00:00:00.000Z", level2.GetLivePreviewConfig().PreviewTimestamp); - Assert.NotEqual("2024-11-30T00:00:00.000Z", level3.GetLivePreviewConfig().PreviewTimestamp); - } - - #endregion - } -} \ No newline at end of file diff --git a/Contentstack.Core.Unit.Tests/ContentstackClientResetTests.cs b/Contentstack.Core.Unit.Tests/ContentstackClientResetTests.cs deleted file mode 100644 index 0c60767a..00000000 --- a/Contentstack.Core.Unit.Tests/ContentstackClientResetTests.cs +++ /dev/null @@ -1,476 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using AutoFixture; -using Contentstack.Core.Configuration; -using Contentstack.Core.Unit.Tests.Helpers; -using Contentstack.Core.Unit.Tests.Mokes; -using Newtonsoft.Json.Linq; -using Xunit; - -namespace Contentstack.Core.Unit.Tests -{ - /// - /// Unit tests for ContentstackClient.ResetLivePreview() method - /// Tests timeline state clearing, configuration preservation, and edge cases - /// - [Trait("Category", "TimelinePreview")] - [Trait("Category", "Reset")] - public class ContentstackClientResetTests : ContentstackClientTestBase - { - #region Positive Test Cases - - [Fact] - public void ResetLivePreview_ClearsTimelineProperties() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Set timeline properties - config.PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - config.ReleaseId = "test_release_123"; - - // Act - client.ResetLivePreview(); - - // Assert - Assert.Null(config.PreviewTimestamp); - Assert.Null(config.ReleaseId); - } - - [Fact] - public void ResetLivePreview_ClearsPreviewResponse() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - config.PreviewResponse = CreateMockPreviewResponse(); - - // Verify response is set - Assert.NotNull(config.PreviewResponse); - - // Act - client.ResetLivePreview(); - - // Assert - Assert.Null(config.PreviewResponse); - } - - [Fact] - public void ResetLivePreview_ClearsFingerprintProperties() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Set fingerprint properties - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", "fingerprint_timestamp"); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", "fingerprint_release"); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", "fingerprint_hash"); - - // Act - client.ResetLivePreview(); - - // Assert - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp")); - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintReleaseId")); - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintLivePreview")); - } - - [Fact] - public void ResetLivePreview_ClearsContentTypeContext() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Set content type context - SetInternalProperty(config, "ContentTypeUID", "test_content_type"); - - // Act - client.ResetLivePreview(); - - // Assert - Assert.Null(GetInternalProperty(config, "ContentTypeUID")); - } - - [Fact] - public void ResetLivePreview_ClearsEntryContext() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Set entry context - SetInternalProperty(config, "EntryUID", "test_entry"); - - // Act - client.ResetLivePreview(); - - // Assert - Assert.Null(GetInternalProperty(config, "EntryUID")); - } - - [Fact] - public void ResetLivePreview_ClearsLivePreviewHash() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Set live preview hash - SetInternalProperty(config, "LivePreview", "test_hash_123"); - - // Act - client.ResetLivePreview(); - - // Assert - Assert.Null(GetInternalProperty(config, "LivePreview")); - } - - [Fact] - public void ResetLivePreview_PreservesBaseConfiguration() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - var originalEnable = config.Enable; - var originalHost = config.Host; - var originalManagementToken = config.ManagementToken; - var originalPreviewToken = config.PreviewToken; - - // Act - client.ResetLivePreview(); - - // Assert - Base configuration should be preserved - Assert.Equal(originalEnable, config.Enable); - Assert.Equal(originalHost, config.Host); - Assert.Equal(originalManagementToken, config.ManagementToken); - Assert.Equal(originalPreviewToken, config.PreviewToken); - } - - [Fact] - public void ResetLivePreview_PreservesClientConfiguration() - { - // Arrange - var client = CreateClientWithTimeline(); - - var originalApiKey = client.GetApplicationKey(); - var originalAccessToken = client.GetAccessToken(); - var originalEnvironment = client.GetEnvironment(); - var originalVersion = client.GetVersion(); - - // Act - client.ResetLivePreview(); - - // Assert - Client configuration should be preserved - Assert.Equal(originalApiKey, client.GetApplicationKey()); - Assert.Equal(originalAccessToken, client.GetAccessToken()); - Assert.Equal(originalEnvironment, client.GetEnvironment()); - Assert.Equal(originalVersion, client.GetVersion()); - } - - [Fact] - public void ResetLivePreview_MultipleCallsIdempotent() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Set up timeline state - config.PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - config.ReleaseId = "test_release"; - config.PreviewResponse = CreateMockPreviewResponse(); - - // Act - Multiple resets - client.ResetLivePreview(); - client.ResetLivePreview(); - client.ResetLivePreview(); - - // Assert - Should be safe to call multiple times - Assert.Null(config.PreviewTimestamp); - Assert.Null(config.ReleaseId); - Assert.Null(config.PreviewResponse); - } - - [Fact] - public void ResetLivePreview_AfterComplexTimelineOperations() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Set up complex timeline state - config.PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - config.ReleaseId = "complex_release_123"; - config.PreviewResponse = JObject.Parse(@"{ - ""entry"": { - ""uid"": ""complex_entry"", - ""title"": ""Complex Test Entry"", - ""nested"": { - ""deep"": { - ""structure"": ""value"" - } - }, - ""array"": [1, 2, 3, 4, 5] - } - }"); - - SetInternalProperty(config, "ContentTypeUID", "complex_ct"); - SetInternalProperty(config, "EntryUID", "complex_entry"); - SetInternalProperty(config, "LivePreview", "complex_hash"); - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", "complex_timestamp"); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", "complex_release"); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", "complex_hash"); - - // Act - client.ResetLivePreview(); - - // Assert - All complex state cleared - Assert.Null(config.PreviewTimestamp); - Assert.Null(config.ReleaseId); - Assert.Null(config.PreviewResponse); - Assert.Null(GetInternalProperty(config, "ContentTypeUID")); - Assert.Null(GetInternalProperty(config, "EntryUID")); - Assert.Null(GetInternalProperty(config, "LivePreview")); - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp")); - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintReleaseId")); - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintLivePreview")); - } - - [Fact] - public async Task ResetLivePreview_AfterLivePreviewQuery_ClearsAllState() - { - // Arrange - var client = CreateClientWithLivePreview(); - var mockHandler = new TimelineMockHttpHandler().ForSuccessfulLivePreview(); - client.Plugins.Add(mockHandler); - - var query = CreateLivePreviewQuery( - contentTypeUid: "reset_test_ct", - entryUid: "reset_test_entry", - previewTimestamp: "2024-11-29T14:30:00.000Z", - releaseId: "reset_test_release" - ); - - // Execute live preview query to set up state - await client.LivePreviewQueryAsync(query); - - var config = client.GetLivePreviewConfig(); - - // Verify state is set - Assert.Equal("2024-11-29T14:30:00.000Z", config.PreviewTimestamp); - Assert.Equal("reset_test_release", config.ReleaseId); - - // Act - client.ResetLivePreview(); - - // Assert - All state cleared - Assert.Null(config.PreviewTimestamp); - Assert.Null(config.ReleaseId); - Assert.Null(config.PreviewResponse); - Assert.Null(GetInternalProperty(config, "ContentTypeUID")); - Assert.Null(GetInternalProperty(config, "EntryUID")); - Assert.Null(GetInternalProperty(config, "LivePreview")); - } - - [Fact] - public void ResetLivePreview_DoesNotAffectForkedClients() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var forkedClient = parentClient.Fork(); - - // Set different timeline states - parentClient.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T10:00:00.000Z"; - forkedClient.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T14:00:00.000Z"; - - // Act - Reset parent client only - parentClient.ResetLivePreview(); - - // Assert - Parent is reset but fork is unaffected - Assert.Null(parentClient.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("2024-11-29T14:00:00.000Z", forkedClient.GetLivePreviewConfig().PreviewTimestamp); - } - - #endregion - - #region Negative Test Cases - - [Fact] - public void ResetLivePreview_WithNullConfig_HandlesGracefully() - { - // Arrange - var client = CreateClient(); - SetInternalProperty(client, "LivePreviewConfig", null); - - // Act & Assert - Should not throw - var exception = Record.Exception(() => client.ResetLivePreview()); - Assert.Null(exception); - } - - [Fact] - public void ResetLivePreview_DisabledLivePreview_NoException() - { - // Arrange - var client = CreateClientWithLivePreview(enabled: false); - - // Act & Assert - Should not throw - var exception = Record.Exception(() => client.ResetLivePreview()); - Assert.Null(exception); - } - - [Fact] - public void ResetLivePreview_AlreadyClearedState_HandlesCorrectly() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Clear all state first - config.PreviewTimestamp = null; - config.ReleaseId = null; - config.PreviewResponse = null; - SetInternalProperty(config, "ContentTypeUID", null); - SetInternalProperty(config, "EntryUID", null); - SetInternalProperty(config, "LivePreview", null); - - // Act & Assert - Should not throw with already cleared state - var exception = Record.Exception(() => client.ResetLivePreview()); - Assert.Null(exception); - - // Verify state remains cleared - Assert.Null(config.PreviewTimestamp); - Assert.Null(config.ReleaseId); - Assert.Null(config.PreviewResponse); - } - - [Fact] - public void ResetLivePreview_WithCorruptedState_HandlesGracefully() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Create corrupted state - config.PreviewTimestamp = "invalid-timestamp-format"; - config.PreviewResponse = JObject.Parse("{}"); // Empty invalid response - - // Act & Assert - Should not throw with corrupted state - var exception = Record.Exception(() => client.ResetLivePreview()); - Assert.Null(exception); - - // Assert - Corrupted state is cleared - Assert.Null(config.PreviewTimestamp); - Assert.Null(config.PreviewResponse); - } - - #endregion - - #region Performance and Edge Cases - - [Fact] - public void ResetLivePreview_Performance_FastExecution() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Set up state to reset - config.PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - config.ReleaseId = "perf_test_release"; - config.PreviewResponse = CreateMockPreviewResponse(); - - var iterations = 1000; - var startTime = DateTime.UtcNow; - - // Act - Multiple resets to test performance - for (int i = 0; i < iterations; i++) - { - // Set some state - config.PreviewTimestamp = $"2024-11-{(i % 12) + 1:D2}-01T00:00:00.000Z"; - config.ReleaseId = $"perf_release_{i}"; - - // Reset - client.ResetLivePreview(); - } - - var duration = DateTime.UtcNow - startTime; - - // Assert - Should be very fast (under 100ms for 1000 resets) - Assert.True(duration.TotalMilliseconds < 100, - $"ResetLivePreview took {duration.TotalMilliseconds}ms for {iterations} operations"); - } - - [Fact] - public void ResetLivePreview_ConcurrentCalls_ThreadSafe() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - config.PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - - var tasks = new Task[10]; - - // Act - Concurrent reset calls - for (int i = 0; i < tasks.Length; i++) - { - tasks[i] = Task.Run(() => - { - try - { - client.ResetLivePreview(); - } - catch (Exception ex) - { - // Should not throw - throw new Exception($"Concurrent reset failed: {ex.Message}", ex); - } - }); - } - - // Assert - All tasks should complete without exception - var exception = Record.Exception(() => Task.WaitAll(tasks)); - Assert.Null(exception); - - // Final state should be cleared - Assert.Null(config.PreviewTimestamp); - Assert.Null(config.ReleaseId); - Assert.Null(config.PreviewResponse); - } - - [Fact] - public void ResetLivePreview_MemoryEfficiency_ReleasesReferences() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Create large objects to test memory release - var largeResponse = JObject.Parse(@"{ - ""entry"": { - ""large_field"": """ + new string('x', 10000) + @""", - ""another_large_field"": """ + new string('y', 10000) + @""" - } - }"); - - config.PreviewResponse = largeResponse; - - // Act - client.ResetLivePreview(); - - // Force garbage collection - largeResponse = null; - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - - // Assert - References should be cleared - Assert.Null(config.PreviewResponse); - } - - #endregion - } -} \ No newline at end of file diff --git a/Contentstack.Core.Unit.Tests/ContentstackClientUnitTests.cs b/Contentstack.Core.Unit.Tests/ContentstackClientUnitTests.cs index 8495bf66..f5073b8b 100644 --- a/Contentstack.Core.Unit.Tests/ContentstackClientUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/ContentstackClientUnitTests.cs @@ -8,24 +8,31 @@ using Contentstack.Core.Configuration; using Contentstack.Core.Internals; using Contentstack.Core.Models; -using Contentstack.Core.Unit.Tests.Helpers; using Contentstack.Core.Unit.Tests.Mokes; using Microsoft.Extensions.Options; -using Newtonsoft.Json.Linq; using Xunit; namespace Contentstack.Core.Unit.Tests { /// - /// Comprehensive unit tests for ContentstackClient class - /// Includes Timeline Preview functionality: Fork(), ResetLivePreview(), LivePreviewQueryAsync() - /// Uses mocks and AutoFixture, no real API calls + /// Unit tests for ContentstackClient class - uses mocks and AutoFixture, no real API calls + /// Follows Management SDK pattern /// - [Trait("Category", "TimelinePreview")] - [Trait("Category", "Unit")] - [Trait("Category", "Fast")] - public class ContentstackClientUnitTests : ContentstackClientTestBase + public class ContentstackClientUnitTests { + private readonly IFixture _fixture = new Fixture(); + + private ContentstackClient CreateClient(string environment = null, string apiKey = null, string deliveryToken = null, string version = null) + { + var options = new ContentstackOptions() + { + ApiKey = apiKey ?? _fixture.Create(), + DeliveryToken = deliveryToken ?? _fixture.Create(), + Environment = environment ?? _fixture.Create(), + Version = version + }; + return new ContentstackClient(new OptionsWrapper(options)); + } [Fact] public void GetEnvironment_ReturnsEnvironment() @@ -160,9 +167,10 @@ public void GetAccessToken_WithAccessToken_ReturnsAccessToken() var options = new ContentstackOptions() { ApiKey = _fixture.Create(), - AccessToken = accessToken, Environment = _fixture.Create() }; + typeof(ContentstackOptions).GetProperty("AccessToken")! + .SetValue(options, accessToken); var client = new ContentstackClient(new OptionsWrapper(options)); // Act @@ -804,7 +812,7 @@ public void GetHeader_WithLocalHeaderAndNoStackHeaders_ReturnsLocalHeader() } [Fact] - public void LivePreviewQueryAsync_WithContentTypeUid_SetsContentTypeUID() + public async Task LivePreviewQueryAsync_WithContentTypeUid_SetsContentTypeUID() { // Arrange var client = CreateClient(); @@ -814,15 +822,14 @@ public void LivePreviewQueryAsync_WithContentTypeUid_SetsContentTypeUID() }; // Act - var task = client.LivePreviewQueryAsync(query); - task.Wait(); + await client.LivePreviewQueryAsync(query); // Assert Assert.Equal("test_ct_uid", client.LivePreviewConfig.ContentTypeUID); } [Fact] - public void LivePreviewQueryAsync_WithEntryUid_SetsEntryUID() + public async Task LivePreviewQueryAsync_WithEntryUid_SetsEntryUID() { // Arrange var client = CreateClient(); @@ -832,15 +839,14 @@ public void LivePreviewQueryAsync_WithEntryUid_SetsEntryUID() }; // Act - var task = client.LivePreviewQueryAsync(query); - task.Wait(); + await client.LivePreviewQueryAsync(query); // Assert Assert.Equal("test_entry_uid", client.LivePreviewConfig.EntryUID); } [Fact] - public void LivePreviewQueryAsync_WithLivePreview_SetsLivePreview() + public async Task LivePreviewQueryAsync_WithLivePreview_SetsLivePreview() { // Arrange var client = CreateClient(); @@ -850,15 +856,14 @@ public void LivePreviewQueryAsync_WithLivePreview_SetsLivePreview() }; // Act - var task = client.LivePreviewQueryAsync(query); - task.Wait(); + await client.LivePreviewQueryAsync(query); // Assert Assert.Equal("test_hash", client.LivePreviewConfig.LivePreview); } [Fact] - public void LivePreviewQueryAsync_WithReleaseId_SetsReleaseId() + public async Task LivePreviewQueryAsync_WithReleaseId_SetsReleaseId() { // Arrange var client = CreateClient(); @@ -868,15 +873,14 @@ public void LivePreviewQueryAsync_WithReleaseId_SetsReleaseId() }; // Act - var task = client.LivePreviewQueryAsync(query); - task.Wait(); + await client.LivePreviewQueryAsync(query); // Assert Assert.Equal("test_release_id", client.LivePreviewConfig.ReleaseId); } [Fact] - public void LivePreviewQueryAsync_WithPreviewTimestamp_SetsPreviewTimestamp() + public async Task LivePreviewQueryAsync_WithPreviewTimestamp_SetsPreviewTimestamp() { // Arrange var client = CreateClient(); @@ -886,15 +890,14 @@ public void LivePreviewQueryAsync_WithPreviewTimestamp_SetsPreviewTimestamp() }; // Act - var task = client.LivePreviewQueryAsync(query); - task.Wait(); + await client.LivePreviewQueryAsync(query); // Assert Assert.Equal("test_timestamp", client.LivePreviewConfig.PreviewTimestamp); } [Fact] - public void LivePreviewQueryAsync_WithoutContentTypeUid_UsesCurrentContentTypeUid() + public async Task LivePreviewQueryAsync_WithoutContentTypeUid_UsesCurrentContentTypeUid() { // Arrange var client = CreateClient(); @@ -906,15 +909,14 @@ public void LivePreviewQueryAsync_WithoutContentTypeUid_UsesCurrentContentTypeUi var query = new Dictionary(); // Act - var task = client.LivePreviewQueryAsync(query); - task.Wait(); + await client.LivePreviewQueryAsync(query); // Assert Assert.Equal(currentContentTypeUid, client.LivePreviewConfig.ContentTypeUID); } [Fact] - public void LivePreviewQueryAsync_WithoutEntryUid_UsesCurrentEntryUid() + public async Task LivePreviewQueryAsync_WithoutEntryUid_UsesCurrentEntryUid() { // Arrange var client = CreateClient(); @@ -926,15 +928,14 @@ public void LivePreviewQueryAsync_WithoutEntryUid_UsesCurrentEntryUid() var query = new Dictionary(); // Act - var task = client.LivePreviewQueryAsync(query); - task.Wait(); + await client.LivePreviewQueryAsync(query); // Assert Assert.Equal(currentEntryUid, client.LivePreviewConfig.EntryUID); } [Fact] - public void LivePreviewQueryAsync_ClearsLivePreviewConfig() + public async Task LivePreviewQueryAsync_ClearsLivePreviewConfig() { // Arrange var client = CreateClient(); @@ -945,1449 +946,14 @@ public void LivePreviewQueryAsync_ClearsLivePreviewConfig() var query = new Dictionary(); // Act - var task = client.LivePreviewQueryAsync(query); - task.Wait(); - - // Assert - Assert.Null(client.LivePreviewConfig.LivePreview); - Assert.Null(client.LivePreviewConfig.PreviewTimestamp); - Assert.Null(client.LivePreviewConfig.ReleaseId); - } - - #region Timeline Preview - Fork() Comprehensive Tests - - [Fact] - public void Fork_CreatesNewInstance_NotSameReference() - { - // Arrange - var client = CreateClient(); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.NotSame(client, forkedClient); - } - - [Fact] - public void Fork_CreatesIndependentClient_DifferentIdentity() - { - // Arrange - var client = CreateClient(); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.NotSame(client, forkedClient); - Assert.NotEqual(client.GetHashCode(), forkedClient.GetHashCode()); - } - - [Fact] - public void Fork_PreservesApiKey_ExactMatch() - { - // Arrange - var apiKey = "test_api_key"; - var client = CreateClient(apiKey: apiKey); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.Equal(client.GetApplicationKey(), forkedClient.GetApplicationKey()); - } - - [Fact] - public void Fork_PreservesAccessToken_ExactMatch() - { - // Arrange - var client = CreateClient(); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.Equal(client.GetAccessToken(), forkedClient.GetAccessToken()); - } - - [Fact] - public void Fork_PreservesEnvironment_ExactMatch() - { - // Arrange - var environment = "test_environment"; - var client = CreateClient(environment: environment); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.Equal(client.GetEnvironment(), forkedClient.GetEnvironment()); - } - - [Fact] - public void Fork_PreservesHost_ExactMatch() - { - // Arrange - var client = CreateClient(); - // For testing purposes, we verify that both client and fork return consistent host values - - // Act - var forkedClient = client.Fork(); - var originalHost = GetHost(client); - var forkedHost = GetHost(forkedClient); - - // Assert - Assert.Equal(originalHost, forkedHost); - Assert.NotNull(originalHost); // Ensure it's not null - } - - [Fact] - public void Fork_PreservesTimeout_ExactMatch() - { - // Arrange - var client = CreateClient(); - SetTimeout(client, 30000); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.Equal(GetTimeout(client), GetTimeout(forkedClient)); - } - - [Fact] - public void Fork_PreservesRegion_ExactMatch() - { - // Arrange - var client = CreateClient(); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.Equal(GetRegion(client), GetRegion(forkedClient)); - } - - [Fact] - public void Fork_PreservesVersion_ExactMatch() - { - // Arrange - var version = "v3"; - var client = CreateClient(version: version); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.Equal(client.GetVersion(), forkedClient.GetVersion()); - } - - [Fact] - public void Fork_PreservesBranch_ExactMatch() - { - // Arrange - var client = CreateClient(); - SetBranch(client, "test_branch"); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.Equal(GetBranch(client), GetBranch(forkedClient)); - } - - [Fact] - public void Fork_ClonesLivePreviewConfig_NotSameReference() - { - // Arrange - var client = CreateClientWithLivePreview(); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.NotSame(client.GetLivePreviewConfig(), forkedClient.GetLivePreviewConfig()); - } - - [Fact] - public void Fork_PreservesLivePreviewEnable_ExactMatch() - { - // Arrange - var client = CreateClientWithLivePreview(enabled: true); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.Equal(client.GetLivePreviewConfig().Enable, forkedClient.GetLivePreviewConfig().Enable); - } - - [Fact] - public void Fork_PreservesLivePreviewHost_ExactMatch() - { - // Arrange - var host = "custom.preview.host.com"; - var client = CreateClientWithLivePreview(host: host); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.Equal(client.GetLivePreviewConfig().Host, forkedClient.GetLivePreviewConfig().Host); - } - - [Fact] - public void Fork_PreservesManagementToken_ExactMatch() - { - // Arrange - var client = CreateClientWithLivePreview(); - client.GetLivePreviewConfig().ManagementToken = "test_mgmt_token"; - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.Equal(client.GetLivePreviewConfig().ManagementToken, forkedClient.GetLivePreviewConfig().ManagementToken); - } - - [Fact] - public void Fork_PreservesPreviewToken_ExactMatch() - { - // Arrange - var client = CreateClientWithLivePreview(); - client.GetLivePreviewConfig().PreviewToken = "test_preview_token"; - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.Equal(client.GetLivePreviewConfig().PreviewToken, forkedClient.GetLivePreviewConfig().PreviewToken); - } - - [Fact] - public void Fork_PreservesReleaseId_ExactMatch() - { - // Arrange - var client = CreateClientWithTimeline(releaseId: "test_release_123"); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.Equal(client.GetLivePreviewConfig().ReleaseId, forkedClient.GetLivePreviewConfig().ReleaseId); - } - - [Fact] - public void Fork_PreservesPreviewTimestamp_ExactMatch() - { - // Arrange - var timestamp = "2024-11-29T14:30:00.000Z"; - var client = CreateClientWithTimeline(timestamp: timestamp); - - // Act - var forkedClient = client.Fork(); - - // Assert - Assert.Equal(client.GetLivePreviewConfig().PreviewTimestamp, forkedClient.GetLivePreviewConfig().PreviewTimestamp); - } - - [Fact] - public void Fork_PreservesLivePreview_ExactMatch() - { - // Arrange - var hash = "test_hash_456"; - var client = CreateClientWithTimeline(hash: hash); - - // Act - var forkedClient = client.Fork(); - - // Assert - var parentProperty = typeof(LivePreviewConfig).GetProperty("LivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var forkedProperty = typeof(LivePreviewConfig).GetProperty("LivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - - var parentValue = parentProperty?.GetValue(client.GetLivePreviewConfig()); - var forkedValue = forkedProperty?.GetValue(forkedClient.GetLivePreviewConfig()); - - Assert.Equal(parentValue, forkedValue); - } - - [Fact] - public void Fork_PreservesContentTypeUID_ExactMatch() - { - // Arrange - var client = CreateClientWithTimeline(); - - // Act - var forkedClient = client.Fork(); - - // Assert - var parentProperty = typeof(LivePreviewConfig).GetProperty("ContentTypeUID", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var forkedProperty = typeof(LivePreviewConfig).GetProperty("ContentTypeUID", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - - var parentValue = parentProperty?.GetValue(client.GetLivePreviewConfig()); - var forkedValue = forkedProperty?.GetValue(forkedClient.GetLivePreviewConfig()); - - Assert.Equal(parentValue, forkedValue); - } - - [Fact] - public void Fork_PreservesEntryUID_ExactMatch() - { - // Arrange - var client = CreateClientWithTimeline(); - - // Act - var forkedClient = client.Fork(); - - // Assert - var parentProperty = typeof(LivePreviewConfig).GetProperty("EntryUID", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var forkedProperty = typeof(LivePreviewConfig).GetProperty("EntryUID", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - - var parentValue = parentProperty?.GetValue(client.GetLivePreviewConfig()); - var forkedValue = forkedProperty?.GetValue(forkedClient.GetLivePreviewConfig()); - - Assert.Equal(parentValue, forkedValue); - } - - [Fact] - public void Fork_PreservesPreviewResponse_SameReference() - { - // Arrange - var client = CreateClientWithLivePreview(); - var previewResponse = TimelineMockHelpers.CreateMockLivePreviewResponse(); - client.GetLivePreviewConfig().PreviewResponse = Newtonsoft.Json.Linq.JObject.Parse(previewResponse); - - // Act - var forkedClient = client.Fork(); - - // Assert - PreviewResponse should be shared reference for memory efficiency - Assert.Same(client.GetLivePreviewConfig().PreviewResponse, forkedClient.GetLivePreviewConfig().PreviewResponse); - } - - [Fact] - public void Fork_PreservesAllFingerprints_ExactMatch() - { - // Arrange - var client = CreateClientWithLivePreview(); - var config = client.GetLivePreviewConfig(); - - // Set fingerprints using reflection - typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintPreviewTimestamp", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(config, "fingerprint_timestamp"); - typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintReleaseId", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(config, "fingerprint_release"); - typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintLivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(config, "fingerprint_hash"); - - // Act - var forkedClient = client.Fork(); - var forkedConfig = forkedClient.GetLivePreviewConfig(); + await client.LivePreviewQueryAsync(query); // Assert - var parentTimestamp = typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintPreviewTimestamp", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(config); - var forkedTimestamp = typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintPreviewTimestamp", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(forkedConfig); - - Assert.Equal(parentTimestamp, forkedTimestamp); - } - - #endregion - - #region Timeline Preview - Fork() Isolation Tests - - [Fact] - public void Fork_CopiesAllHeaders_ExactMatch() - { - // Arrange - var client = CreateClient(); - client.SetHeader("custom-header", "test-value"); - client.SetHeader("another-header", "another-value"); - - // Act - var forkedClient = client.Fork(); - - // Assert - Headers should be copied (we can't directly access them, so test by setting different values) - forkedClient.SetHeader("custom-header", "modified-value"); - - // Parent should still have original value (test via reflection or observable behavior) - // This is a behavioral test - the key is that they're independent - Assert.NotSame(client, forkedClient); - } - - [Fact] - public void Fork_ModifyParentHeaders_DoesNotAffectForked() - { - // Arrange - var client = CreateClient(); - client.SetHeader("test-header", "original-value"); - var forkedClient = client.Fork(); - - // Act - client.SetHeader("test-header", "modified-value"); - - // Assert - This tests isolation behavior - Assert.NotSame(client, forkedClient); + Assert.Null(client.LivePreviewConfig.LivePreview); + Assert.Null(client.LivePreviewConfig.PreviewTimestamp); + Assert.Null(client.LivePreviewConfig.ReleaseId); } - [Fact] - public void Fork_ModifyForkedHeaders_DoesNotAffectParent() - { - // Arrange - var client = CreateClient(); - client.SetHeader("test-header", "original-value"); - var forkedClient = client.Fork(); - - // Act - forkedClient.SetHeader("test-header", "modified-value"); - forkedClient.SetHeader("new-header", "new-value"); - - // Assert - This tests isolation behavior - Assert.NotSame(client, forkedClient); - } - - [Fact] - public void Fork_ModifyParentLivePreviewConfig_DoesNotAffectForked() - { - // Arrange - var client = CreateClientWithTimeline(); - var forkedClient = client.Fork(); - - // Act - Modify parent config - client.GetLivePreviewConfig().ReleaseId = "modified_release"; - client.GetLivePreviewConfig().PreviewTimestamp = "modified_timestamp"; - - // Assert - Forked config should be unaffected - Assert.NotEqual(client.GetLivePreviewConfig().ReleaseId, forkedClient.GetLivePreviewConfig().ReleaseId); - Assert.NotEqual(client.GetLivePreviewConfig().PreviewTimestamp, forkedClient.GetLivePreviewConfig().PreviewTimestamp); - } - - [Fact] - public void Fork_ModifyForkedLivePreviewConfig_DoesNotAffectParent() - { - // Arrange - var client = CreateClientWithTimeline(); - var originalReleaseId = client.GetLivePreviewConfig().ReleaseId; - var originalTimestamp = client.GetLivePreviewConfig().PreviewTimestamp; - var forkedClient = client.Fork(); - - // Act - Modify forked config - forkedClient.GetLivePreviewConfig().ReleaseId = "forked_release"; - forkedClient.GetLivePreviewConfig().PreviewTimestamp = "forked_timestamp"; - - // Assert - Parent config should be unaffected - Assert.Equal(originalReleaseId, client.GetLivePreviewConfig().ReleaseId); - Assert.Equal(originalTimestamp, client.GetLivePreviewConfig().PreviewTimestamp); - } - - [Fact] - public void Fork_ModifyParentPreviewResponse_AffectsBothDueToSharedReference() - { - // Arrange - var client = CreateClientWithLivePreview(); - var mockResponse = Newtonsoft.Json.Linq.JObject.Parse(TimelineMockHelpers.CreateMockLivePreviewResponse()); - client.GetLivePreviewConfig().PreviewResponse = mockResponse; - var forkedClient = client.Fork(); - - // Act - Modify the shared JObject - mockResponse["modified"] = "test"; - - // Assert - Both should see the change since it's a shared reference - Assert.Same(client.GetLivePreviewConfig().PreviewResponse, forkedClient.GetLivePreviewConfig().PreviewResponse); - Assert.Equal("test", client.GetLivePreviewConfig().PreviewResponse["modified"]?.ToString()); - Assert.Equal("test", forkedClient.GetLivePreviewConfig().PreviewResponse["modified"]?.ToString()); - } - - [Fact] - public void Fork_ParentResetLivePreview_DoesNotAffectForked() - { - // Arrange - var client = CreateClientWithTimeline(); - var forkedClient = client.Fork(); - var forkedReleaseId = forkedClient.GetLivePreviewConfig().ReleaseId; - - // Act - Reset parent's live preview - client.ResetLivePreview(); - - // Assert - Forked client should be unaffected - Assert.Null(client.GetLivePreviewConfig().ReleaseId); - Assert.Equal(forkedReleaseId, forkedClient.GetLivePreviewConfig().ReleaseId); - } - - [Fact] - public void Fork_ForkedResetLivePreview_DoesNotAffectParent() - { - // Arrange - var client = CreateClientWithTimeline(); - var parentReleaseId = client.GetLivePreviewConfig().ReleaseId; - var forkedClient = client.Fork(); - - // Act - Reset forked client's live preview - forkedClient.ResetLivePreview(); - - // Assert - Parent client should be unaffected - Assert.Null(forkedClient.GetLivePreviewConfig().ReleaseId); - Assert.Equal(parentReleaseId, client.GetLivePreviewConfig().ReleaseId); - } - - [Fact] - public void Fork_WithNullLivePreviewConfig_CreatesDefault() - { - // Arrange - var client = CreateClient(); // No LivePreview config - - // Act - var forkedClient = client.Fork(); - - // Assert - Should not throw and should have default config - Assert.NotNull(forkedClient.GetLivePreviewConfig()); - Assert.False(forkedClient.GetLivePreviewConfig().Enable); - } - - [Fact] - public void Fork_MultipleForks_AllIndependent() - { - // Arrange - var client = CreateClientWithTimeline(); - - // Act - var fork1 = client.Fork(); - var fork2 = client.Fork(); - var fork3 = fork1.Fork(); // Fork of a fork - - // Assert - All should be independent - Assert.NotSame(client, fork1); - Assert.NotSame(client, fork2); - Assert.NotSame(client, fork3); - Assert.NotSame(fork1, fork2); - Assert.NotSame(fork1, fork3); - Assert.NotSame(fork2, fork3); - - // Modify one - others should be unaffected - fork1.GetLivePreviewConfig().ReleaseId = "fork1_modified"; - - Assert.NotEqual(fork1.GetLivePreviewConfig().ReleaseId, client.GetLivePreviewConfig().ReleaseId); - Assert.NotEqual(fork1.GetLivePreviewConfig().ReleaseId, fork2.GetLivePreviewConfig().ReleaseId); - Assert.NotEqual(fork1.GetLivePreviewConfig().ReleaseId, fork3.GetLivePreviewConfig().ReleaseId); - } - - [Fact] - public void Fork_NestedForks_MaintainIndependence() - { - // Arrange - var grandparent = CreateClientWithTimeline(releaseId: "grandparent_release"); - var parent = grandparent.Fork(); - parent.GetLivePreviewConfig().ReleaseId = "parent_release"; - var child = parent.Fork(); - child.GetLivePreviewConfig().ReleaseId = "child_release"; - - // Act & Assert - Each should maintain its own state - Assert.Equal("grandparent_release", grandparent.GetLivePreviewConfig().ReleaseId); - Assert.Equal("parent_release", parent.GetLivePreviewConfig().ReleaseId); - Assert.Equal("child_release", child.GetLivePreviewConfig().ReleaseId); - - // Modify child - should not affect parent or grandparent - child.GetLivePreviewConfig().ReleaseId = "modified_child"; - - Assert.Equal("grandparent_release", grandparent.GetLivePreviewConfig().ReleaseId); - Assert.Equal("parent_release", parent.GetLivePreviewConfig().ReleaseId); - Assert.Equal("modified_child", child.GetLivePreviewConfig().ReleaseId); - } - - #endregion - - #region Timeline Preview - ResetLivePreview() Complete State Management - - [Fact] - public void ResetLivePreview_ClearsLivePreview_SetsNull() - { - // Arrange - var client = CreateClientWithTimeline(hash: "test_hash"); - - // Act - client.ResetLivePreview(); - - // Assert - var property = typeof(LivePreviewConfig).GetProperty("LivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var value = property?.GetValue(client.GetLivePreviewConfig()); - Assert.Null(value); - } - - [Fact] - public void ResetLivePreview_ClearsReleaseId_SetsNull() - { - // Arrange - var client = CreateClientWithTimeline(releaseId: "test_release"); - - // Act - client.ResetLivePreview(); - - // Assert - Assert.Null(client.GetLivePreviewConfig().ReleaseId); - } - - [Fact] - public void ResetLivePreview_ClearsPreviewTimestamp_SetsNull() - { - // Arrange - var client = CreateClientWithTimeline(timestamp: "2024-11-29T14:30:00.000Z"); - - // Act - client.ResetLivePreview(); - - // Assert - Assert.Null(client.GetLivePreviewConfig().PreviewTimestamp); - } - - [Fact] - public void ResetLivePreview_ClearsContentTypeUID_SetsNull() - { - // Arrange - var client = CreateClientWithTimeline(); - - // Act - client.ResetLivePreview(); - - // Assert - var property = typeof(LivePreviewConfig).GetProperty("ContentTypeUID", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var value = property?.GetValue(client.GetLivePreviewConfig()); - Assert.Null(value); - } - - [Fact] - public void ResetLivePreview_ClearsEntryUID_SetsNull() - { - // Arrange - var client = CreateClientWithTimeline(); - - // Act - client.ResetLivePreview(); - - // Assert - var property = typeof(LivePreviewConfig).GetProperty("EntryUID", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var value = property?.GetValue(client.GetLivePreviewConfig()); - Assert.Null(value); - } - - [Fact] - public void ResetLivePreview_ClearsPreviewResponse_SetsNull() - { - // Arrange - var client = CreateClientWithLivePreview(); - client.GetLivePreviewConfig().PreviewResponse = Newtonsoft.Json.Linq.JObject.Parse("{\"test\": \"value\"}"); - - // Act - client.ResetLivePreview(); - - // Assert - Assert.Null(client.GetLivePreviewConfig().PreviewResponse); - } - - [Fact] - public void ResetLivePreview_ClearsPreviewTimestampFingerprint_SetsNull() - { - // Arrange - var client = CreateClientWithLivePreview(); - var property = typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintPreviewTimestamp", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - property?.SetValue(client.GetLivePreviewConfig(), "test_fingerprint"); - - // Act - client.ResetLivePreview(); - - // Assert - var value = property?.GetValue(client.GetLivePreviewConfig()); - Assert.Null(value); - } - - [Fact] - public void ResetLivePreview_ClearsReleaseIdFingerprint_SetsNull() - { - // Arrange - var client = CreateClientWithLivePreview(); - var property = typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintReleaseId", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - property?.SetValue(client.GetLivePreviewConfig(), "test_fingerprint"); - - // Act - client.ResetLivePreview(); - - // Assert - var value = property?.GetValue(client.GetLivePreviewConfig()); - Assert.Null(value); - } - - [Fact] - public void ResetLivePreview_ClearsLivePreviewFingerprint_SetsNull() - { - // Arrange - var client = CreateClientWithLivePreview(); - var property = typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintLivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - property?.SetValue(client.GetLivePreviewConfig(), "test_fingerprint"); - - // Act - client.ResetLivePreview(); - - // Assert - var value = property?.GetValue(client.GetLivePreviewConfig()); - Assert.Null(value); - } - - [Fact] - public void ResetLivePreview_PreservesEnable_NoChange() - { - // Arrange - var client = CreateClientWithLivePreview(enabled: true); - var originalEnable = client.GetLivePreviewConfig().Enable; - - // Act - client.ResetLivePreview(); - - // Assert - Assert.Equal(originalEnable, client.GetLivePreviewConfig().Enable); - } - - [Fact] - public void ResetLivePreview_PreservesHost_NoChange() - { - // Arrange - var host = "custom.preview.host.com"; - var client = CreateClientWithLivePreview(host: host); - - // Act - client.ResetLivePreview(); - - // Assert - Assert.Equal(host, client.GetLivePreviewConfig().Host); - } - - [Fact] - public void ResetLivePreview_PreservesManagementToken_NoChange() - { - // Arrange - var client = CreateClientWithLivePreview(); - var token = "test_mgmt_token"; - client.GetLivePreviewConfig().ManagementToken = token; - - // Act - client.ResetLivePreview(); - - // Assert - Assert.Equal(token, client.GetLivePreviewConfig().ManagementToken); - } - - [Fact] - public void ResetLivePreview_PreservesPreviewToken_NoChange() - { - // Arrange - var client = CreateClientWithLivePreview(); - var token = "test_preview_token"; - client.GetLivePreviewConfig().PreviewToken = token; - - // Act - client.ResetLivePreview(); - - // Assert - Assert.Equal(token, client.GetLivePreviewConfig().PreviewToken); - } - - [Fact] - public void ResetLivePreview_AfterReset_IsCachedPreviewReturnsFalse() - { - // Arrange - var client = CreateClientWithTimeline(); - // Set up a scenario that would normally return true for IsCachedPreviewForCurrentQuery - var config = client.GetLivePreviewConfig(); - config.PreviewResponse = Newtonsoft.Json.Linq.JObject.Parse("{\"test\": \"value\"}"); - - // Set matching fingerprints - typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintPreviewTimestamp", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(config, config.PreviewTimestamp); - typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintReleaseId", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(config, config.ReleaseId); - typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintLivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(config, typeof(LivePreviewConfig).GetProperty("LivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(config)); - - // Verify it would return true before reset - Assert.True(config.IsCachedPreviewForCurrentQuery()); - - // Act - client.ResetLivePreview(); - - // Assert - Assert.False(config.IsCachedPreviewForCurrentQuery()); - } - - [Fact] - public void ResetLivePreview_AfterReset_NoTimelineState() - { - // Arrange - var client = CreateClientWithTimeline(); - - // Act - client.ResetLivePreview(); - - // Assert - Verify complete timeline state is cleared - TimelineAssertionHelpers.VerifyTimelineStateCleared(client.GetLivePreviewConfig(), "After ResetLivePreview"); - } - - [Fact] - public void ResetLivePreview_AfterReset_NoFingerprintState() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Set some fingerprints - typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintPreviewTimestamp", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(config, "test_fingerprint"); - typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintReleaseId", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(config, "test_fingerprint"); - typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintLivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(config, "test_fingerprint"); - - // Act - client.ResetLivePreview(); - - // Assert - All fingerprints should be null - Assert.Null(typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintPreviewTimestamp", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(config)); - Assert.Null(typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintReleaseId", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(config)); - Assert.Null(typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintLivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(config)); - } - - [Fact] - public void ResetLivePreview_CalledMultipleTimes_RemainsClean() - { - // Arrange - var client = CreateClientWithTimeline(); - - // Act - Call multiple times - client.ResetLivePreview(); - client.ResetLivePreview(); - client.ResetLivePreview(); - - // Assert - Should remain clean - TimelineAssertionHelpers.VerifyTimelineStateCleared(client.GetLivePreviewConfig(), "After multiple ResetLivePreview calls"); - } - - [Fact] - public void ResetLivePreview_WithAlreadyNullValues_NoChange() - { - // Arrange - var client = CreateClientWithLivePreview(); // No timeline values set - - // Act - client.ResetLivePreview(); - - // Assert - Should handle gracefully - TimelineAssertionHelpers.VerifyTimelineStateCleared(client.GetLivePreviewConfig(), "Reset with already null values"); - } - - [Fact] - public async Task ResetLivePreview_DuringLivePreviewQuery_SafeExecution() - { - // Arrange - var client = CreateClientWithMockHandler(TimelineMockHelpers.CreateMockLivePreviewResponse()); - var query = CreateLivePreviewQuery(contentTypeUid: "test_ct", entryUid: "test_entry"); - - // Act - Start LivePreviewQueryAsync and immediately reset (simulate concurrent access) - var queryTask = client.LivePreviewQueryAsync(query); - client.ResetLivePreview(); // This should not cause issues - await queryTask; - - // Assert - Should not throw exceptions - Assert.NotNull(client.GetLivePreviewConfig()); - } - - #endregion - - #region Timeline Preview - LivePreviewQueryAsync() Complete Async Behavior - - [Fact] - public async Task LivePreviewQueryAsync_WithContentTypeUid_Enhanced_SetsContentTypeUID() - { - // Arrange - var client = CreateClientWithLivePreview(); - var query = CreateLivePreviewQuery(contentTypeUid: "enhanced_ct_uid"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - var property = typeof(LivePreviewConfig).GetProperty("ContentTypeUID", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var value = property?.GetValue(client.GetLivePreviewConfig()); - Assert.Equal("enhanced_ct_uid", value); - } - - [Fact] - public async Task LivePreviewQueryAsync_WithEntryUid_Enhanced_SetsEntryUID() - { - // Arrange - var client = CreateClientWithLivePreview(); - var query = CreateLivePreviewQuery(entryUid: "enhanced_entry_uid"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - var property = typeof(LivePreviewConfig).GetProperty("EntryUID", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var value = property?.GetValue(client.GetLivePreviewConfig()); - Assert.Equal("enhanced_entry_uid", value); - } - - [Fact] - public async Task LivePreviewQueryAsync_WithLivePreview_Enhanced_SetsLivePreview() - { - // Arrange - var client = CreateClientWithLivePreview(); - var query = CreateLivePreviewQuery(livePreview: "enhanced_hash"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - var property = typeof(LivePreviewConfig).GetProperty("LivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var value = property?.GetValue(client.GetLivePreviewConfig()); - Assert.Equal("enhanced_hash", value); - } - - [Fact] - public async Task LivePreviewQueryAsync_WithReleaseId_Enhanced_SetsReleaseId() - { - // Arrange - var client = CreateClientWithLivePreview(); - var query = CreateLivePreviewQuery(releaseId: "enhanced_release_id"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Assert.Equal("enhanced_release_id", client.GetLivePreviewConfig().ReleaseId); - } - - [Fact] - public async Task LivePreviewQueryAsync_WithPreviewTimestamp_Enhanced_SetsPreviewTimestamp() - { - // Arrange - var client = CreateClientWithLivePreview(); - var query = CreateLivePreviewQuery(previewTimestamp: "2024-11-29T14:30:00.000Z"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Assert.Equal("2024-11-29T14:30:00.000Z", client.GetLivePreviewConfig().PreviewTimestamp); - } - - [Fact] - public async Task LivePreviewQueryAsync_WithAllParameters_SetsAllFields() - { - // Arrange - var client = CreateClientWithLivePreview(); - var query = CreateLivePreviewQuery( - contentTypeUid: "all_ct", - entryUid: "all_entry", - livePreview: "all_hash", - releaseId: "all_release", - previewTimestamp: "2024-11-29T14:30:00.000Z" - ); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - var config = client.GetLivePreviewConfig(); - Assert.Equal("all_release", config.ReleaseId); - Assert.Equal("2024-11-29T14:30:00.000Z", config.PreviewTimestamp); - - var ctProperty = typeof(LivePreviewConfig).GetProperty("ContentTypeUID", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - Assert.Equal("all_ct", ctProperty?.GetValue(config)); - - var entryProperty = typeof(LivePreviewConfig).GetProperty("EntryUID", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - Assert.Equal("all_entry", entryProperty?.GetValue(config)); - - var hashProperty = typeof(LivePreviewConfig).GetProperty("LivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - Assert.Equal("all_hash", hashProperty?.GetValue(config)); - } - - [Fact] - public async Task LivePreviewQueryAsync_WithEmptyStringValues_NormalizesToNull() - { - // Arrange - var client = CreateClientWithLivePreview(); - var query = CreateLivePreviewQuery( - contentTypeUid: "", - entryUid: "", - livePreview: "", - releaseId: "", - previewTimestamp: "" - ); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - SDK normalizes empty strings to null - var config = client.GetLivePreviewConfig(); - Assert.Null(config.ReleaseId); - Assert.Null(config.PreviewTimestamp); - } - - [Fact] - public async Task LivePreviewQueryAsync_WithNullValues_SetsNull() - { - // Arrange - var client = CreateClientWithLivePreview(); - var query = new Dictionary - { - ["content_type_uid"] = null, - ["entry_uid"] = null, - ["live_preview"] = null, - ["release_id"] = null, - ["preview_timestamp"] = null - }; - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - var config = client.GetLivePreviewConfig(); - Assert.Null(config.ReleaseId); - Assert.Null(config.PreviewTimestamp); - } - - [Fact] - public async Task LivePreviewQueryAsync_NoContentTypeUid_UsesCurrentContentTypeUid_Enhanced() - { - // Arrange - var client = CreateClientWithLivePreview(); - - // Use a query with all required parameters - var query = new Dictionary - { - ["content_type_uid"] = "test_content_type", - ["entry_uid"] = "test_entry", - ["live_preview"] = "test_hash" - }; - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Configuration should be set from query parameters - var config = client.GetLivePreviewConfig(); - Assert.Equal("test_content_type", config.ContentTypeUID); - Assert.Equal("test_entry", config.EntryUID); - } - - [Fact] - public async Task LivePreviewQueryAsync_NoEntryUid_UsesCurrentEntryUid_Enhanced() - { - // Arrange - var client = CreateClientWithLivePreview(); - - // Use a query with all required parameters - var query = new Dictionary - { - ["content_type_uid"] = "test_content_type", - ["entry_uid"] = "test_entry", - ["live_preview"] = "test_hash" - }; - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Configuration should be set from query parameters - var config = client.GetLivePreviewConfig(); - Assert.Equal("test_content_type", config.ContentTypeUID); - Assert.Equal("test_entry", config.EntryUID); - } - - [Fact] - public async Task LivePreviewQueryAsync_NoCurrentValues_LeavesNull() - { - // Arrange - var client = CreateClientWithLivePreview(); - // Create a truly empty query without default values - var query = new Dictionary(); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - var config = client.GetLivePreviewConfig(); - var ctProperty = typeof(LivePreviewConfig).GetProperty("ContentTypeUID", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var entryProperty = typeof(LivePreviewConfig).GetProperty("EntryUID", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - - Assert.Null(ctProperty?.GetValue(config)); - Assert.Null(entryProperty?.GetValue(config)); - } - - [Fact] - public async Task LivePreviewQueryAsync_EmptyQuery_ClearsAllFields() - { - // Arrange - var client = CreateClientWithTimeline(); // Start with timeline values - var query = new Dictionary(); // Empty query - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - All timeline fields should be cleared - TimelineAssertionHelpers.VerifyTimelineStateCleared(client.GetLivePreviewConfig(), "After empty query"); - } - - [Fact] - public async Task LivePreviewQueryAsync_ClearsLivePreview_BeforeProcessing() - { - // Arrange - var client = CreateClientWithTimeline(hash: "old_hash"); - var query = CreateLivePreviewQuery(livePreview: "new_hash"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - var property = typeof(LivePreviewConfig).GetProperty("LivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var value = property?.GetValue(client.GetLivePreviewConfig()); - Assert.Equal("new_hash", value); - } - - [Fact] - public async Task LivePreviewQueryAsync_ClearsPreviewTimestamp_BeforeProcessing() - { - // Arrange - var client = CreateClientWithTimeline(timestamp: "old_timestamp"); - var query = CreateLivePreviewQuery(previewTimestamp: "new_timestamp"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Assert.Equal("new_timestamp", client.GetLivePreviewConfig().PreviewTimestamp); - } - - [Fact] - public async Task LivePreviewQueryAsync_ClearsReleaseId_BeforeProcessing() - { - // Arrange - var client = CreateClientWithTimeline(releaseId: "old_release"); - var query = CreateLivePreviewQuery(releaseId: "new_release"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Assert.Equal("new_release", client.GetLivePreviewConfig().ReleaseId); - } - - [Fact] - public async Task LivePreviewQueryAsync_ClearsPreviewResponse_BeforeProcessing() - { - // Arrange - var client = CreateClientWithLivePreview(); - client.GetLivePreviewConfig().PreviewResponse = Newtonsoft.Json.Linq.JObject.Parse("{\"old\": \"response\"}"); - var query = CreateLivePreviewQuery(); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Should be cleared at start, might be set by prefetch - Assert.True(true); // The clearing happens regardless of prefetch outcome - } - - [Fact] - public async Task LivePreviewQueryAsync_ClearsAllFingerprints_BeforeProcessing() - { - // Arrange - var client = CreateClientWithLivePreview(); - var config = client.GetLivePreviewConfig(); - - // Set old fingerprints - typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintPreviewTimestamp", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(config, "old_timestamp_fingerprint"); - typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintReleaseId", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(config, "old_release_fingerprint"); - typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintLivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(config, "old_hash_fingerprint"); - - var query = CreateLivePreviewQuery(); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Fingerprints should be cleared (and potentially reset by prefetch) - Assert.True(true); // The clearing happens regardless of prefetch outcome - } - - [Fact] - public async Task LivePreviewQueryAsync_FromDirtyState_CleansCompletely() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Set up dirty state with old values and fingerprints - config.PreviewResponse = Newtonsoft.Json.Linq.JObject.Parse("{\"dirty\": \"state\"}"); - typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintPreviewTimestamp", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) - ?.SetValue(config, "dirty_fingerprint"); - - var query = CreateLivePreviewQuery(previewTimestamp: "clean_timestamp"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Assert.Equal("clean_timestamp", config.PreviewTimestamp); - } - - [Fact] - public async Task LivePreviewQueryAsync_EnabledFalse_SkipsPrefetch() - { - // Arrange - var client = CreateClientWithLivePreview(enabled: false); - var mockHandler = new TimelineMockHttpHandler().ForSuccessfulLivePreview(); - client.Plugins.Add(mockHandler); - - var query = CreateLivePreviewQuery(contentTypeUid: "test_ct", entryUid: "test_entry"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - No network call should be made - Assert.Empty(mockHandler.Requests); - Assert.Null(client.GetLivePreviewConfig().PreviewResponse); - } - - [Fact] - public async Task LivePreviewQueryAsync_HostNull_SkipsPrefetch() - { - // Arrange - Create client with live preview enabled but explicitly set host to null after creation - var client = CreateClientWithLivePreview(enabled: true); - client.GetLivePreviewConfig().Host = null; // Explicitly set host to null - var mockHandler = new TimelineMockHttpHandler().ForSuccessfulLivePreview(); - client.Plugins.Add(mockHandler); - - var query = CreateLivePreviewQuery(contentTypeUid: "test_ct", entryUid: "test_entry"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - No network call should be made when host is null - Assert.Empty(mockHandler.Requests); - } - - [Fact] - public async Task LivePreviewQueryAsync_HostEmpty_SkipsPrefetch() - { - // Arrange - var client = CreateClientWithLivePreview(enabled: true, host: ""); - var mockHandler = new TimelineMockHttpHandler().ForSuccessfulLivePreview(); - client.Plugins.Add(mockHandler); - - var query = CreateLivePreviewQuery(contentTypeUid: "test_ct", entryUid: "test_entry"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - No network call should be made - Assert.Empty(mockHandler.Requests); - } - - [Fact] - public async Task LivePreviewQueryAsync_AllConditionsMet_AttemptsPrefetch() - { - // Arrange - var client = CreateClientWithLivePreview(enabled: true); - var mockHandler = new TimelineMockHttpHandler().ForSuccessfulLivePreview("test_entry", "test_ct"); - client.Plugins.Add(mockHandler); - - var query = CreateLivePreviewQuery(contentTypeUid: "test_ct", entryUid: "test_entry"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Network call should be made - Assert.NotEmpty(mockHandler.Requests); - } - - [Fact] - public async Task LivePreviewQueryAsync_SuccessfulPrefetch_AttemptsNetworkCall() - { - // Arrange - var client = CreateClientWithLivePreview(); - var mockResponse = TimelineMockHelpers.CreateMockLivePreviewResponse("test_entry", "test_ct"); - var mockHandler = new TimelineMockHttpHandler().ForLivePreview(JObject.Parse(mockResponse)); - client.Plugins.Add(mockHandler); - - var query = CreateLivePreviewQuery(contentTypeUid: "test_ct", entryUid: "test_entry"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Verify that prefetch was attempted (network call made) - Assert.NotEmpty(mockHandler.Requests); - - // Verify that basic config is set regardless of prefetch success/failure - var config = client.GetLivePreviewConfig(); - Assert.Equal("test_ct", config.ContentTypeUID); - Assert.Equal("test_entry", config.EntryUID); - } - - [Fact] - public async Task LivePreviewQueryAsync_SuccessfulPrefetch_SetsAllFingerprints() - { - // Arrange - var client = CreateClientWithLivePreview(); - var mockResponse = TimelineMockHelpers.CreateMockLivePreviewResponse("test_entry", "test_ct"); - var mockHandler = new TimelineMockHttpHandler().ForLivePreview(JObject.Parse(mockResponse)); - client.Plugins.Add(mockHandler); - - var query = CreateLivePreviewQuery( - contentTypeUid: "test_ct", - entryUid: "test_entry", - previewTimestamp: "2024-11-29T14:30:00.000Z", - releaseId: "test_release", - livePreview: "test_hash" - ); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - var config = client.GetLivePreviewConfig(); - if (config.PreviewResponse != null) // Only check if prefetch was successful - { - var timestampFingerprint = typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintPreviewTimestamp", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(config); - var releaseFingerprint = typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintReleaseId", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(config); - var hashFingerprint = typeof(LivePreviewConfig).GetProperty("PreviewResponseFingerprintLivePreview", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(config); - - Assert.Equal("2024-11-29T14:30:00.000Z", timestampFingerprint); - Assert.Equal("test_release", releaseFingerprint); - Assert.Equal("test_hash", hashFingerprint); - } - } - - [Fact] - public async Task LivePreviewQueryAsync_PrefetchThrowsException_SwallowsException() - { - // Arrange - var client = CreateClientWithLivePreview(); - var mockHandler = new TimelineMockHttpHandler().ThrowTimeout(); - client.Plugins.Add(mockHandler); - - var query = CreateLivePreviewQuery(contentTypeUid: "test_ct", entryUid: "test_entry"); - - // Act & Assert - Should not throw - await client.LivePreviewQueryAsync(query); - - // Should complete without exceptions - Assert.NotNull(client.GetLivePreviewConfig()); - } - - [Fact] - public async Task LivePreviewQueryAsync_NetworkError_SwallowsException() - { - // Arrange - var client = CreateClientWithLivePreview(); - var mockHandler = new TimelineMockHttpHandler().ThrowWebException("Network error"); - client.Plugins.Add(mockHandler); - - var query = CreateLivePreviewQuery(contentTypeUid: "test_ct", entryUid: "test_entry"); - - // Act & Assert - Should not throw - await client.LivePreviewQueryAsync(query); - - // Should complete without exceptions - Assert.NotNull(client.GetLivePreviewConfig()); - } - - [Fact] - public async Task LivePreviewQueryAsync_ExecutesAsynchronously_ReturnsTask() - { - // Arrange - var client = CreateClientWithLivePreview(); - var query = CreateLivePreviewQuery(); - - // Act - var task = client.LivePreviewQueryAsync(query); - - // Assert - Assert.IsAssignableFrom(task); - await task; // Should complete - } - - [Fact] - public async Task LivePreviewQueryAsync_CanAwait_CompletesSuccessfully() - { - // Arrange - var client = CreateClientWithLivePreview(); - var query = CreateLivePreviewQuery(previewTimestamp: "2024-11-29T14:30:00.000Z"); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Assert.Equal("2024-11-29T14:30:00.000Z", client.GetLivePreviewConfig().PreviewTimestamp); - } - - [Fact] - public async Task LivePreviewQueryAsync_MultipleSimultaneous_HandledCorrectly() - { - // Arrange - var client = CreateClientWithLivePreview(); - var query1 = CreateLivePreviewQuery(previewTimestamp: "timestamp1"); - var query2 = CreateLivePreviewQuery(previewTimestamp: "timestamp2"); - - // Act - Run simultaneously - var task1 = client.LivePreviewQueryAsync(query1); - var task2 = client.LivePreviewQueryAsync(query2); - await Task.WhenAll(task1, task2); - - // Assert - Should handle concurrent access gracefully (final state may be from either) - Assert.True(client.GetLivePreviewConfig().PreviewTimestamp == "timestamp1" || - client.GetLivePreviewConfig().PreviewTimestamp == "timestamp2"); - } - - #endregion - [Fact] public void GetHeader_WithLocalHeaderAndEmptyStackHeaders_ReturnsLocalHeader() { diff --git a/Contentstack.Core.Unit.Tests/ContentstackOptionsUnitTests.cs b/Contentstack.Core.Unit.Tests/ContentstackOptionsUnitTests.cs index c6e228ad..7c9aee40 100644 --- a/Contentstack.Core.Unit.Tests/ContentstackOptionsUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/ContentstackOptionsUnitTests.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using System.Reflection; using AutoFixture; using Contentstack.Core.Configuration; using Contentstack.Core.Internals; @@ -69,15 +70,12 @@ public void AccessToken_SetAndGet_ReturnsCorrectValue() var options = new ContentstackOptions(); var accessToken = _fixture.Create(); - // Act -#pragma warning disable CS0618 // Type or member is obsolete - options.AccessToken = accessToken; -#pragma warning restore CS0618 // Type or member is obsolete + // Act — use reflection so we still validate the deprecated property without CS0618 at compile time + var accessTokenProp = typeof(ContentstackOptions).GetProperty("AccessToken")!; + accessTokenProp.SetValue(options, accessToken); // Assert -#pragma warning disable CS0618 // Type or member is obsolete - Assert.Equal(accessToken, options.AccessToken); -#pragma warning restore CS0618 // Type or member is obsolete + Assert.Equal(accessToken, accessTokenProp.GetValue(options)); } [Fact] diff --git a/Contentstack.Core.Unit.Tests/ContentstackRegionUnitTests.cs b/Contentstack.Core.Unit.Tests/ContentstackRegionUnitTests.cs index 70dcc54d..f6c4fe2a 100644 --- a/Contentstack.Core.Unit.Tests/ContentstackRegionUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/ContentstackRegionUnitTests.cs @@ -184,7 +184,7 @@ public void ContentstackRegion_GetValues_ReturnsAllRegions() // Assert Assert.NotNull(values); - Assert.Equal(6, values.Length); + Assert.Equal(7, values.Length); var regionArray = values.Cast().ToArray(); Assert.Contains(ContentstackRegion.US, regionArray); Assert.Contains(ContentstackRegion.EU, regionArray); @@ -192,6 +192,7 @@ public void ContentstackRegion_GetValues_ReturnsAllRegions() Assert.Contains(ContentstackRegion.AZURE_NA, regionArray); Assert.Contains(ContentstackRegion.GCP_NA, regionArray); Assert.Contains(ContentstackRegion.AU, regionArray); + Assert.Contains(ContentstackRegion.GCP_EU, regionArray); } #endregion diff --git a/Contentstack.Core.Unit.Tests/EntryUnitTests.cs b/Contentstack.Core.Unit.Tests/EntryUnitTests.cs index 859cd063..37b11862 100644 --- a/Contentstack.Core.Unit.Tests/EntryUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/EntryUnitTests.cs @@ -10,7 +10,7 @@ using Contentstack.Core.Models; using Contentstack.Core.Unit.Tests.Mokes; using Microsoft.Extensions.Options; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; using Xunit; namespace Contentstack.Core.Unit.Tests @@ -838,31 +838,31 @@ public void GetMetadata_WithoutMetadata_ReturnsNull() #region ToJson Tests [Fact] - public void ToJson_ReturnsJObject() + public void ToJson_ReturnsJsonObject() { // Arrange var entry = CreateEntry(); var jObjectField = typeof(Entry).GetField("jObject", BindingFlags.NonPublic | BindingFlags.Instance); - var jObject = new JObject + var jsonObject = new JsonObject { - { "uid", "test_entry_uid" }, - { "title", "Test Entry" }, - { "content_type_uid", "source" } + ["uid"] = "test_entry_uid", + ["title"] = "Test Entry", + ["content_type_uid"] = "source" }; - jObjectField?.SetValue(entry, jObject); + jObjectField?.SetValue(entry, jsonObject); // Act - JObject result = entry.ToJson(); + JsonObject result = entry.ToJson(); // Assert Assert.NotNull(result); - Assert.Equal("test_entry_uid", result["uid"].ToString()); - Assert.Equal("Test Entry", result["title"].ToString()); + Assert.Equal("test_entry_uid", result["uid"]?.GetValue()); + Assert.Equal("Test Entry", result["title"]?.GetValue()); } [Fact] - public void ToJson_WithNullJObject_ReturnsNull() + public void ToJson_WithNullJsonObject_ReturnsNull() { // Arrange var entry = CreateEntry(); @@ -871,7 +871,7 @@ public void ToJson_WithNullJObject_ReturnsNull() jObjectField?.SetValue(entry, null); // Act - JObject result = entry.ToJson(); + JsonObject result = entry.ToJson(); // Assert Assert.Null(result); @@ -1915,13 +1915,13 @@ public void ParseObject_WithMetadata_ParsesMetadata() var entry = CreateEntry(); var parseObjectMethod = typeof(Entry).GetMethod("ParseObject", BindingFlags.NonPublic | BindingFlags.Instance); - var json = new JObject + var json = new JsonObject { ["title"] = "Test Title", - ["_metadata"] = new JObject + ["_metadata"] = new JsonObject { ["uid"] = "test_uid", - ["tags"] = new JArray("tag1", "tag2") + ["tags"] = new JsonArray("tag1", "tag2") } }; @@ -1942,7 +1942,7 @@ public void ParseObject_WithoutMetadata_DoesNotSetMetadata() var entry = CreateEntry(); var parseObjectMethod = typeof(Entry).GetMethod("ParseObject", BindingFlags.NonPublic | BindingFlags.Instance); - var json = new JObject + var json = new JsonObject { ["title"] = "Test Title" }; diff --git a/Contentstack.Core.Unit.Tests/Helpers/ContentstackClientTestBase.cs b/Contentstack.Core.Unit.Tests/Helpers/ContentstackClientTestBase.cs deleted file mode 100644 index 6ec0d172..00000000 --- a/Contentstack.Core.Unit.Tests/Helpers/ContentstackClientTestBase.cs +++ /dev/null @@ -1,424 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using AutoFixture; -using Contentstack.Core.Configuration; -using Contentstack.Core.Unit.Tests.Mokes; -using Microsoft.Extensions.Options; -using Newtonsoft.Json.Linq; -using Xunit; - -namespace Contentstack.Core.Unit.Tests.Helpers -{ - /// - /// Base class for ContentstackClient unit tests providing common setup and helper methods - /// - public abstract class ContentstackClientTestBase - { - protected readonly IFixture _fixture = new Fixture(); - - /// - /// Creates a basic ContentstackClient with default configuration for testing - /// - /// ContentstackClient instance with test configuration - protected ContentstackClient CreateClient() - { - var options = new ContentstackOptions - { - ApiKey = _fixture.Create(), - DeliveryToken = _fixture.Create(), - Environment = _fixture.Create(), - Host = "cdn.contentstack.io" - }; - - return new ContentstackClient(options); - } - - /// - /// Creates a ContentstackClient with specified environment - /// - /// Environment name - /// ContentstackClient with specified environment - protected ContentstackClient CreateClient(string environment) - { - var options = new ContentstackOptions - { - ApiKey = _fixture.Create(), - DeliveryToken = _fixture.Create(), - Environment = environment, - Host = "cdn.contentstack.io" - }; - - return new ContentstackClient(options); - } - - /// - /// Creates a ContentstackClient with named parameters (for backward compatibility) - /// - protected ContentstackClient CreateClient(string apiKey = null, string deliveryToken = null, string environment = null, string version = null) - { - var options = new ContentstackOptions - { - ApiKey = apiKey ?? _fixture.Create(), - DeliveryToken = deliveryToken ?? _fixture.Create(), - Environment = environment ?? _fixture.Create(), - Host = "cdn.contentstack.io" - }; - - return new ContentstackClient(options); - } - - /// - /// Creates a ContentstackClient with LivePreview configuration - /// - /// Whether LivePreview should be enabled - /// Optional management token - /// Optional preview token - /// Optional host override - /// ContentstackClient with LivePreview configuration - protected ContentstackClient CreateClientWithLivePreview(bool enabled = true, string managementToken = null, string previewToken = null, string host = null) - { - var options = new ContentstackOptions - { - ApiKey = _fixture.Create(), - DeliveryToken = _fixture.Create(), - Environment = _fixture.Create(), - Host = "cdn.contentstack.io", - LivePreview = new LivePreviewConfig - { - Enable = enabled, - Host = host ?? (enabled ? "rest-preview.contentstack.com" : null), - ManagementToken = managementToken ?? (enabled ? _fixture.Create() : null), - PreviewToken = previewToken - } - }; - - return new ContentstackClient(options); - } - - /// - /// Creates a ContentstackClient configured for timeline operations - /// - /// ContentstackClient with timeline-ready configuration - protected ContentstackClient CreateClientWithTimeline() - { - var client = CreateClientWithLivePreview(enabled: true); - var config = client.GetLivePreviewConfig(); - - // Set up basic timeline context - config.PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - config.ReleaseId = "test_release_123"; - - return client; - } - - /// - /// Creates a ContentstackClient configured for timeline operations (named parameters for backward compatibility) - /// - protected ContentstackClient CreateClientWithTimeline(string releaseId = null, string timestamp = null, string hash = null) - { - var client = CreateClientWithLivePreview(enabled: true); - var config = client.GetLivePreviewConfig(); - - // Set up timeline context - config.PreviewTimestamp = timestamp ?? "2024-11-29T14:30:00.000Z"; - config.ReleaseId = releaseId ?? "test_release_123"; - - if (!string.IsNullOrEmpty(hash)) - { - SetInternalProperty(config, "LivePreview", hash); - } - - return client; - } - - /// - /// Creates a live preview query dictionary with specified parameters - /// - /// Content type UID - /// Entry UID - /// Preview timestamp - /// Release ID - /// Live preview hash - /// Dictionary with live preview query parameters - protected Dictionary CreateLivePreviewQuery( - string contentTypeUid = "test_ct", - string entryUid = "test_entry", - string previewTimestamp = null, - string releaseId = null, - string livePreview = "init") - { - var query = new Dictionary - { - ["content_type_uid"] = contentTypeUid, - ["entry_uid"] = entryUid, - ["live_preview"] = livePreview - }; - - if (!string.IsNullOrEmpty(previewTimestamp)) - query["preview_timestamp"] = previewTimestamp; - - if (!string.IsNullOrEmpty(releaseId)) - query["release_id"] = releaseId; - - return query; - } - - /// - /// Sets internal property value using reflection (for testing internal state) - /// - /// Target object - /// Property name - /// Value to set - protected void SetInternalProperty(object target, string propertyName, object value) - { - var property = target.GetType().GetProperty(propertyName, - BindingFlags.NonPublic | BindingFlags.Instance); - property?.SetValue(target, value); - } - - /// - /// Gets internal property value using reflection (for testing internal state) - /// - /// Expected return type - /// Target object - /// Property name - /// Property value - protected T GetInternalProperty(object target, string propertyName) - { - var property = target.GetType().GetProperty(propertyName, - BindingFlags.NonPublic | BindingFlags.Instance); - return (T)property?.GetValue(target); - } - - /// - /// Sets internal field value using reflection (for testing internal state) - /// - /// Target object - /// Field name - /// Value to set - protected void SetInternalField(object target, string fieldName, object value) - { - var field = target.GetType().GetField(fieldName, - BindingFlags.NonPublic | BindingFlags.Instance); - field?.SetValue(target, value); - } - - /// - /// Gets internal field value using reflection (for testing internal state) - /// - /// Expected return type - /// Target object - /// Field name - /// Field value - protected T GetInternalField(object target, string fieldName) - { - var field = target.GetType().GetField(fieldName, - BindingFlags.NonPublic | BindingFlags.Instance); - return (T)field?.GetValue(target); - } - - /// - /// Creates a mock JObject response for timeline preview testing - /// - /// Entry UID for the mock response - /// Content Type UID for the mock response - /// Mock JObject response - protected JObject CreateMockPreviewResponse(string entryUid = "mock_entry", string contentTypeUid = "mock_ct") - { - return JObject.Parse($@"{{ - ""uid"": ""{entryUid}"", - ""content_type_uid"": ""{contentTypeUid}"", - ""title"": ""Mock Entry Title"", - ""created_at"": ""2024-11-29T14:30:00.000Z"", - ""updated_at"": ""2024-11-29T15:45:00.000Z"", - ""publish_details"": {{ - ""environment"": ""test"", - ""locale"": ""en-us"", - ""time"": ""2024-11-29T16:00:00.000Z"" - }}, - ""mock_field"": ""mock_value_{_fixture.Create()}"" - }}"); - } - - /// - /// Verifies that two clients are independent instances - /// - /// First client - /// Second client - protected void AssertClientsAreIndependent(ContentstackClient client1, ContentstackClient client2) - { - if (client1 == null) throw new ArgumentNullException(nameof(client1)); - if (client2 == null) throw new ArgumentNullException(nameof(client2)); - - // Verify they are different instances - Assert.NotSame(client1, client2); - - // Verify LivePreview configs are different instances - Assert.NotSame(client1.GetLivePreviewConfig(), client2.GetLivePreviewConfig()); - } - - /// - /// Verifies that client configuration is properly preserved - /// - /// Original client - /// New client to verify - protected void AssertConfigurationPreserved(ContentstackClient originalClient, ContentstackClient newClient) - { - Assert.Equal(originalClient.GetApplicationKey(), newClient.GetApplicationKey()); - Assert.Equal(originalClient.GetAccessToken(), newClient.GetAccessToken()); - Assert.Equal(originalClient.GetEnvironment(), newClient.GetEnvironment()); - Assert.Equal(originalClient.GetVersion(), newClient.GetVersion()); - } - - /// - /// Helper method to create a client with mock handler (for backward compatibility) - /// - protected ContentstackClient CreateClientWithMockHandler(object mockHandler) - { - // Simple implementation - just returns a basic client - return CreateClient(); - } - - #region Helper Classes for Backward Compatibility - - /// - /// Mock helpers for timeline testing (stub implementation) - /// - protected static class TimelineMockHelpers - { - public static object CreateSuccessfulMockHandler() - { - return new { Success = true }; - } - - public static object CreateFailureMockHandler() - { - return new { Success = false }; - } - - public static string CreateMockLivePreviewResponse(string entryUid = "test_entry", string contentTypeUid = "test_ct") - { - return $@"{{ - ""entry"": {{ - ""uid"": ""{entryUid}"", - ""content_type_uid"": ""{contentTypeUid}"", - ""title"": ""Mock Timeline Entry"", - ""created_at"": ""2024-11-29T10:00:00.000Z"", - ""updated_at"": ""2024-11-29T14:30:00.000Z"", - ""publish_details"": {{ - ""environment"": ""test"", - ""locale"": ""en-us"", - ""time"": ""2024-11-29T14:30:00.000Z"" - }}, - ""mock_field"": ""timeline_test_value"" - }} - }}"; - } - } - - /// - /// Assertion helpers for timeline testing (stub implementation) - /// - protected static class TimelineAssertionHelpers - { - public static void AssertTimelineStateCleared(ContentstackClient client) - { - var config = client.GetLivePreviewConfig(); - Assert.Null(config?.PreviewTimestamp); - Assert.Null(config?.ReleaseId); - } - - public static void VerifyTimelineStateCleared(LivePreviewConfig config, string message = "") - { - Assert.Null(config?.PreviewTimestamp); - Assert.Null(config?.ReleaseId); - Assert.Null(config?.PreviewResponse); - } - - public static void AssertTimelineStatePreserved(ContentstackClient client, string expectedTimestamp, string expectedReleaseId) - { - var config = client.GetLivePreviewConfig(); - Assert.Equal(expectedTimestamp, config?.PreviewTimestamp); - Assert.Equal(expectedReleaseId, config?.ReleaseId); - } - - public static void AssertCacheState(ContentstackClient client, bool expectedCached) - { - var config = client.GetLivePreviewConfig(); - if (expectedCached) - { - Assert.NotNull(config?.PreviewResponse); - } - else - { - Assert.Null(config?.PreviewResponse); - } - } - } - - #endregion - - #region ContentstackClient Extension Methods for Testing - - /// - /// Mock implementation of SetHost method for testing - /// - protected void SetHost(ContentstackClient client, string host) - { - // Mock implementation - store in a private field or ignore for testing - SetInternalProperty(client, "_mockHost", host); - } - - /// - /// Mock implementation of GetHost method for testing - /// - protected string GetHost(ContentstackClient client) - { - return GetInternalProperty(client, "_mockHost") ?? "cdn.contentstack.io"; - } - - /// - /// Mock implementation of SetTimeout method for testing - /// - protected void SetTimeout(ContentstackClient client, int timeout) - { - SetInternalProperty(client, "_mockTimeout", timeout); - } - - /// - /// Mock implementation of GetTimeout method for testing - /// - protected int GetTimeout(ContentstackClient client) - { - // Provide default timeout value since there's no actual _mockTimeout property - return 30000; // 30 seconds default - } - - /// - /// Mock implementation of GetRegion method for testing - /// - protected string GetRegion(ContentstackClient client) - { - return GetInternalProperty(client, "_mockRegion") ?? "us"; - } - - /// - /// Mock implementation of SetBranch method for testing - /// - protected void SetBranch(ContentstackClient client, string branch) - { - SetInternalProperty(client, "_mockBranch", branch); - } - - /// - /// Mock implementation of GetBranch method for testing - /// - protected string GetBranch(ContentstackClient client) - { - return GetInternalProperty(client, "_mockBranch") ?? "main"; - } - - #endregion - } -} \ No newline at end of file diff --git a/Contentstack.Core.Unit.Tests/Helpers/TimelinePerformanceHelpers.cs b/Contentstack.Core.Unit.Tests/Helpers/TimelinePerformanceHelpers.cs deleted file mode 100644 index 1d1a2e79..00000000 --- a/Contentstack.Core.Unit.Tests/Helpers/TimelinePerformanceHelpers.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using Xunit; - -namespace Contentstack.Core.Unit.Tests.Helpers -{ - /// - /// Helper methods for Timeline Preview performance testing - /// - public static class TimelinePerformanceHelpers - { - /// - /// Measures execution time of a synchronous operation - /// - /// Operation to measure - /// Elapsed time - public static TimeSpan MeasureExecutionTime(Action operation) - { - var stopwatch = Stopwatch.StartNew(); - operation(); - stopwatch.Stop(); - return stopwatch.Elapsed; - } - - /// - /// Measures execution time of an asynchronous operation - /// - /// Async operation to measure - /// Elapsed time - public static async Task MeasureExecutionTime(Func operation) - { - var stopwatch = Stopwatch.StartNew(); - await operation(); - stopwatch.Stop(); - return stopwatch.Elapsed; - } - - /// - /// Asserts that cache operation is significantly faster than network operation - /// - /// Cache operation to test - /// Network operation to compare against - /// Test description for assertion messages - /// Minimum expected speedup factor (default: 5x) - public static async Task AssertCachePerformance( - Func cacheOperation, - Func networkOperation, - string description, - int minimumSpeedupFactor = 5) - { - // Measure cache operation - var cacheTime = await MeasureExecutionTime(cacheOperation); - - // Measure network operation - var networkTime = await MeasureExecutionTime(networkOperation); - - // Calculate speedup factor - var speedupFactor = networkTime.TotalMilliseconds / Math.Max(cacheTime.TotalMilliseconds, 0.001); - - Assert.True(speedupFactor >= minimumSpeedupFactor, - $"{description}: Cache operation ({cacheTime.TotalMilliseconds:F2}ms) should be at least {minimumSpeedupFactor}x faster than network operation ({networkTime.TotalMilliseconds:F2}ms). Actual speedup: {speedupFactor:F2}x"); - } - - /// - /// Asserts that an operation doesn't cause significant memory leaks - /// - /// Operation to test for memory leaks - /// Number of iterations to run - /// Maximum allowed memory growth in bytes - public static void AssertNoMemoryLeak(Action operation, int iterations, long maxMemoryGrowth) - { - // Force initial garbage collection - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - - var initialMemory = GC.GetTotalMemory(true); - - // Run the operation multiple times - for (int i = 0; i < iterations; i++) - { - operation(); - } - - // Force garbage collection again - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - - var finalMemory = GC.GetTotalMemory(true); - var memoryGrowth = finalMemory - initialMemory; - - Assert.True(memoryGrowth <= maxMemoryGrowth, - $"Memory grew by {memoryGrowth:N0} bytes over {iterations} iterations. Maximum allowed: {maxMemoryGrowth:N0} bytes"); - } - - /// - /// Measures average execution time over multiple iterations - /// - /// Operation to measure - /// Number of iterations - /// Average execution time per iteration - public static TimeSpan MeasureAverageExecutionTime(Action operation, int iterations) - { - var totalTime = MeasureExecutionTime(() => - { - for (int i = 0; i < iterations; i++) - { - operation(); - } - }); - - return TimeSpan.FromTicks(totalTime.Ticks / iterations); - } - - /// - /// Measures average execution time of async operations over multiple iterations - /// - /// Async operation to measure - /// Number of iterations - /// Average execution time per iteration - public static async Task MeasureAverageExecutionTime(Func operation, int iterations) - { - var totalTime = await MeasureExecutionTime(async () => - { - for (int i = 0; i < iterations; i++) - { - await operation(); - } - }); - - return TimeSpan.FromTicks(totalTime.Ticks / iterations); - } - - /// - /// Asserts that an operation completes within the specified time limit - /// - /// Operation to test - /// Maximum allowed execution time - /// Description for assertion message - public static void AssertExecutionTime(Action operation, TimeSpan timeLimit, string description) - { - var executionTime = MeasureExecutionTime(operation); - - Assert.True(executionTime <= timeLimit, - $"{description}: Operation took {executionTime.TotalMilliseconds:F2}ms, should be under {timeLimit.TotalMilliseconds:F2}ms"); - } - - /// - /// Asserts that an async operation completes within the specified time limit - /// - /// Async operation to test - /// Maximum allowed execution time - /// Description for assertion message - public static async Task AssertExecutionTime(Func operation, TimeSpan timeLimit, string description) - { - var executionTime = await MeasureExecutionTime(operation); - - Assert.True(executionTime <= timeLimit, - $"{description}: Operation took {executionTime.TotalMilliseconds:F2}ms, should be under {timeLimit.TotalMilliseconds:F2}ms"); - } - } -} \ No newline at end of file diff --git a/Contentstack.Core.Unit.Tests/Helpers/TimelineTestDataBuilder.cs b/Contentstack.Core.Unit.Tests/Helpers/TimelineTestDataBuilder.cs deleted file mode 100644 index 48c9cbe4..00000000 --- a/Contentstack.Core.Unit.Tests/Helpers/TimelineTestDataBuilder.cs +++ /dev/null @@ -1,310 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using AutoFixture; -using Contentstack.Core.Configuration; -using Newtonsoft.Json.Linq; - -namespace Contentstack.Core.Unit.Tests.Helpers -{ - /// - /// Fluent builder for creating Timeline Preview test data and configurations - /// - public class TimelineTestDataBuilder - { - private readonly IFixture _fixture = new Fixture(); - private LivePreviewConfig _config; - - private TimelineTestDataBuilder() - { - _config = new LivePreviewConfig(); - } - - /// - /// Creates a new TimelineTestDataBuilder instance - /// - /// New builder instance - public static TimelineTestDataBuilder New() - { - return new TimelineTestDataBuilder(); - } - - /// - /// Sets the preview timestamp for timeline operations - /// - /// ISO 8601 timestamp string - /// Builder instance for chaining - public TimelineTestDataBuilder WithPreviewTimestamp(string timestamp) - { - _config.PreviewTimestamp = timestamp; - return this; - } - - /// - /// Sets the preview timestamp using a DateTime - /// - /// DateTime to convert to ISO 8601 string - /// Builder instance for chaining - public TimelineTestDataBuilder WithPreviewTimestamp(DateTime dateTime) - { - _config.PreviewTimestamp = dateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); - return this; - } - - /// - /// Sets the release ID for timeline operations - /// - /// Release identifier - /// Builder instance for chaining - public TimelineTestDataBuilder WithReleaseId(string releaseId) - { - _config.ReleaseId = releaseId; - return this; - } - - /// - /// Sets the live preview hash - /// - /// Live preview hash - /// Builder instance for chaining - public TimelineTestDataBuilder WithLivePreview(string hash) - { - SetInternalProperty(_config, "LivePreview", hash); - return this; - } - - /// - /// Sets the content type UID for the configuration - /// - /// Content type identifier - /// Builder instance for chaining - public TimelineTestDataBuilder WithContentTypeUid(string contentTypeUid) - { - SetInternalProperty(_config, "ContentTypeUID", contentTypeUid); - return this; - } - - /// - /// Sets the entry UID for the configuration - /// - /// Entry identifier - /// Builder instance for chaining - public TimelineTestDataBuilder WithEntryUid(string entryUid) - { - SetInternalProperty(_config, "EntryUID", entryUid); - return this; - } - - /// - /// Sets the management token - /// - /// Management token - /// Builder instance for chaining - public TimelineTestDataBuilder WithManagementToken(string token) - { - _config.ManagementToken = token; - return this; - } - - /// - /// Sets the preview token - /// - /// Preview token - /// Builder instance for chaining - public TimelineTestDataBuilder WithPreviewToken(string token) - { - _config.PreviewToken = token; - return this; - } - - /// - /// Enables or disables live preview - /// - /// Whether live preview should be enabled - /// Builder instance for chaining - public TimelineTestDataBuilder WithEnabled(bool enabled) - { - _config.Enable = enabled; - return this; - } - - /// - /// Sets the preview host - /// - /// Preview host URL - /// Builder instance for chaining - public TimelineTestDataBuilder WithHost(string host) - { - _config.Host = host; - return this; - } - - /// - /// Sets a mock preview response - /// - /// JObject response - /// Builder instance for chaining - public TimelineTestDataBuilder WithPreviewResponse(JObject response = null) - { - _config.PreviewResponse = response ?? CreateDefaultPreviewResponse(); - return this; - } - - /// - /// Sets matching fingerprints to create a cache hit scenario - /// - /// Builder instance for chaining - public TimelineTestDataBuilder WithMatchingFingerprints() - { - SetInternalProperty(_config, "PreviewResponseFingerprintPreviewTimestamp", _config.PreviewTimestamp); - SetInternalProperty(_config, "PreviewResponseFingerprintReleaseId", _config.ReleaseId); - SetInternalProperty(_config, "PreviewResponseFingerprintLivePreview", GetInternalProperty(_config, "LivePreview")); - return this; - } - - /// - /// Sets non-matching fingerprints to create a cache miss scenario - /// - /// Builder instance for chaining - public TimelineTestDataBuilder WithNonMatchingFingerprints() - { - SetInternalProperty(_config, "PreviewResponseFingerprintPreviewTimestamp", $"different_{_fixture.Create()}"); - SetInternalProperty(_config, "PreviewResponseFingerprintReleaseId", $"different_{_fixture.Create()}"); - SetInternalProperty(_config, "PreviewResponseFingerprintLivePreview", $"different_{_fixture.Create()}"); - return this; - } - - /// - /// Clears all fingerprints (simulates no previous cache) - /// - /// Builder instance for chaining - public TimelineTestDataBuilder WithNoFingerprints() - { - SetInternalProperty(_config, "PreviewResponseFingerprintPreviewTimestamp", null); - SetInternalProperty(_config, "PreviewResponseFingerprintReleaseId", null); - SetInternalProperty(_config, "PreviewResponseFingerprintLivePreview", null); - return this; - } - - /// - /// Creates a default timeline configuration - /// - /// Builder instance for chaining - public TimelineTestDataBuilder WithDefaultTimelineConfig() - { - return WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("default_release_123") - .WithLivePreview("default_hash_456") - .WithContentTypeUid("default_ct") - .WithEntryUid("default_entry") - .WithEnabled(true) - .WithHost("rest-preview.contentstack.com") - .WithManagementToken(_fixture.Create()); - } - - /// - /// Builds and returns the configured LivePreviewConfig - /// - /// Configured LivePreviewConfig instance - public LivePreviewConfig Build() - { - return _config; - } - - /// - /// Creates a valid preview response JObject for testing - /// - /// Entry UID for the response - /// Content Type UID for the response - /// Mock JObject preview response - public JObject CreateValidPreviewResponse(string entryUid = null, string contentTypeUid = null) - { - entryUid ??= _fixture.Create(); - contentTypeUid ??= _fixture.Create(); - - return JObject.Parse($@"{{ - ""uid"": ""{entryUid}"", - ""content_type_uid"": ""{contentTypeUid}"", - ""title"": ""Timeline Preview Test Entry"", - ""created_at"": ""2024-11-29T10:00:00.000Z"", - ""updated_at"": ""2024-11-29T14:30:00.000Z"", - ""publish_details"": {{ - ""environment"": ""test"", - ""locale"": ""en-us"", - ""time"": ""{_config.PreviewTimestamp ?? "2024-11-29T14:30:00.000Z"}"" - }}, - ""test_field"": ""timeline_test_value_{_fixture.Create()}"", - ""metadata"": {{ - ""timeline_marker"": ""{DateTime.UtcNow.Ticks}"" - }} - }}"); - } - - /// - /// Creates a preview response for a specific timeline scenario - /// - /// Scenario identifier - /// Scenario-specific JObject response - public JObject CreateScenarioResponse(string scenario) - { - var baseResponse = CreateValidPreviewResponse(); - baseResponse["scenario"] = scenario; - baseResponse["timestamp"] = _config.PreviewTimestamp; - baseResponse["release_id"] = _config.ReleaseId; - return baseResponse; - } - - /// - /// Creates a live preview query dictionary from the current configuration - /// - /// Dictionary suitable for LivePreviewQueryAsync - public Dictionary CreateQuery() - { - var query = new Dictionary(); - - var contentTypeUid = GetInternalProperty(_config, "ContentTypeUID"); - var entryUid = GetInternalProperty(_config, "EntryUID"); - var livePreview = GetInternalProperty(_config, "LivePreview"); - - if (!string.IsNullOrEmpty(contentTypeUid)) - query["content_type_uid"] = contentTypeUid; - - if (!string.IsNullOrEmpty(entryUid)) - query["entry_uid"] = entryUid; - - if (!string.IsNullOrEmpty(livePreview)) - query["live_preview"] = livePreview; - - if (!string.IsNullOrEmpty(_config.PreviewTimestamp)) - query["preview_timestamp"] = _config.PreviewTimestamp; - - if (!string.IsNullOrEmpty(_config.ReleaseId)) - query["release_id"] = _config.ReleaseId; - - return query; - } - - #region Private Helper Methods - - private JObject CreateDefaultPreviewResponse() - { - return CreateValidPreviewResponse(); - } - - private void SetInternalProperty(object target, string propertyName, object value) - { - var property = target.GetType().GetProperty(propertyName, - BindingFlags.NonPublic | BindingFlags.Instance); - property?.SetValue(target, value); - } - - private T GetInternalProperty(object target, string propertyName) - { - var property = target.GetType().GetProperty(propertyName, - BindingFlags.NonPublic | BindingFlags.Instance); - return (T)property?.GetValue(target); - } - - #endregion - } -} \ No newline at end of file diff --git a/Contentstack.Core.Unit.Tests/JsonConverterUnitTests.cs b/Contentstack.Core.Unit.Tests/JsonConverterUnitTests.cs index c5036510..bb9f1beb 100644 --- a/Contentstack.Core.Unit.Tests/JsonConverterUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/JsonConverterUnitTests.cs @@ -1,13 +1,13 @@ using System; -using System.IO; -using System.Reflection; +using System.Buffers; +using System.Text; +using System.Text.Json; using AutoFixture; using Contentstack.Core; using Contentstack.Core.Configuration; using Contentstack.Core.Internals; using Contentstack.Core.Models; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using Xunit; namespace Contentstack.Core.Unit.Tests @@ -28,36 +28,33 @@ private ContentstackClient CreateClient() } [Fact] - public void ReadJson_WithValidJson_ReturnsEntry() + public void Read_WithValidJson_ReturnsEntry() { - // Arrange var converter = new EntryJsonConverter(); var json = "{\"uid\":\"test_uid\",\"title\":\"Test Title\"}"; - var reader = new JsonTextReader(new StringReader(json)); + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + reader.Read(); - // Act - var entry = converter.ReadJson(reader, typeof(Entry), null, JsonSerializer.CreateDefault()); + var entry = converter.Read(ref reader, typeof(Entry), CreateClient().SerializerOptions); - // Assert Assert.NotNull(entry); Assert.IsType(entry); } [Fact] - public void WriteJson_WithEntry_DoesNothing() + public void Write_WithEntry_Completes() { - // Arrange var converter = new EntryJsonConverter(); var client = CreateClient(); var contentType = client.ContentType("test_content_type"); var entry = contentType.Entry("test_uid"); - var writer = new JsonTextWriter(new StringWriter()); - - // Act - Should not throw - converter.WriteJson(writer, entry, JsonSerializer.CreateDefault()); + var buffer = new ArrayBufferWriter(); + using (var writer = new Utf8JsonWriter(buffer)) + { + converter.Write(writer, entry, client.SerializerOptions); + } - // Assert - Assert.True(true); + Assert.True(buffer.WrittenCount > 0); } } @@ -77,34 +74,34 @@ private ContentstackClient CreateClient() } [Fact] - public void ReadJson_WithValidJson_ReturnsAsset() + public void Read_WithValidJson_ReturnsAsset() { - // Arrange var converter = new AssetJsonConverter(); var json = "{\"uid\":\"test_uid\",\"title\":\"Test Asset\"}"; - var reader = new JsonTextReader(new StringReader(json)); + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + reader.Read(); - // Act - var asset = converter.ReadJson(reader, typeof(Asset), null, JsonSerializer.CreateDefault()); + var asset = converter.Read(ref reader, typeof(Asset), CreateClient().SerializerOptions); - // Assert Assert.NotNull(asset); Assert.IsType(asset); } [Fact] - public void WriteJson_WithAsset_ThrowsAssetException() + public void Write_WithAsset_ThrowsAssetException() { - // Arrange var converter = new AssetJsonConverter(); var client = CreateClient(); var asset = client.Asset("test_uid"); - var writer = new JsonTextWriter(new StringWriter()); + var buffer = new ArrayBufferWriter(); - // Act & Assert - Assert.Throws(() => - converter.WriteJson(writer, asset, JsonSerializer.CreateDefault())); + Assert.Throws(() => + { + using (var writer = new Utf8JsonWriter(buffer)) + { + converter.Write(writer, asset, client.SerializerOptions); + } + }); } } } - diff --git a/Contentstack.Core.Unit.Tests/LivePreviewConfigFingerprintTests.cs b/Contentstack.Core.Unit.Tests/LivePreviewConfigFingerprintTests.cs deleted file mode 100644 index 9f7c3363..00000000 --- a/Contentstack.Core.Unit.Tests/LivePreviewConfigFingerprintTests.cs +++ /dev/null @@ -1,531 +0,0 @@ -using System; -using AutoFixture; -using Contentstack.Core.Configuration; -using Contentstack.Core.Unit.Tests.Helpers; -using Xunit; - -namespace Contentstack.Core.Unit.Tests -{ - /// - /// Unit tests for LivePreviewConfig.IsCachedPreviewForCurrentQuery() method - /// Tests fingerprint-based cache hit/miss logic for Timeline Preview - /// - [Trait("Category", "TimelinePreview")] - [Trait("Category", "Fingerprint")] - [Trait("Category", "Cache")] - public class LivePreviewConfigFingerprintTests : ContentstackClientTestBase - { - #region Cache Hit Scenarios (All Fingerprints Match) - - [Fact] - public void IsCachedPreviewForCurrentQuery_AllFingerprintsMatch_ReturnsTrue() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("test_release_123") - .WithLivePreview("test_hash_456") - .WithPreviewResponse() - .WithMatchingFingerprints() - .Build(); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.True(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_OnlyTimestampSet_MatchingFingerprint_ReturnsTrue() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithPreviewResponse() - .Build(); - - // Set matching fingerprint only for timestamp - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", config.PreviewTimestamp); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", null); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", null); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.True(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_OnlyReleaseIdSet_MatchingFingerprint_ReturnsTrue() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithReleaseId("test_release_123") - .WithPreviewResponse() - .Build(); - - // Set matching fingerprint only for release ID - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", null); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", config.ReleaseId); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", null); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.True(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_OnlyLivePreviewSet_MatchingFingerprint_ReturnsTrue() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithLivePreview("test_hash_789") - .WithPreviewResponse() - .Build(); - - // Set matching fingerprint only for live preview - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", null); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", null); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", GetInternalProperty(config, "LivePreview")); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.True(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_AllNullValues_MatchingNullFingerprints_ReturnsTrue() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewResponse() - .Build(); - - // Ensure all values and fingerprints are null - config.PreviewTimestamp = null; - config.ReleaseId = null; - SetInternalProperty(config, "LivePreview", null); - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", null); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", null); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", null); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.True(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_EmptyStrings_MatchingEmptyFingerprints_ReturnsTrue() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewResponse() - .Build(); - - // Set empty strings for current values and matching fingerprints - config.PreviewTimestamp = ""; - config.ReleaseId = ""; - SetInternalProperty(config, "LivePreview", ""); - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", ""); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", ""); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", ""); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.True(isCached); - } - - #endregion - - #region Cache Miss Scenarios - - [Fact] - public void IsCachedPreviewForCurrentQuery_NullPreviewResponse_ReturnsFalse() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("test_release_123") - .WithMatchingFingerprints() - .Build(); - - config.PreviewResponse = null; - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_DifferentTimestamp_ReturnsFalse() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("test_release_123") - .WithPreviewResponse() - .Build(); - - // Set different fingerprint for timestamp - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", "2024-11-29T10:00:00.000Z"); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", config.ReleaseId); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", GetInternalProperty(config, "LivePreview")); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_DifferentReleaseId_ReturnsFalse() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("test_release_123") - .WithPreviewResponse() - .Build(); - - // Set different fingerprint for release ID - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", config.PreviewTimestamp); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", "different_release_456"); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", GetInternalProperty(config, "LivePreview")); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_DifferentLivePreviewHash_ReturnsFalse() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("test_release_123") - .WithLivePreview("current_hash") - .WithPreviewResponse() - .Build(); - - // Set different fingerprint for live preview hash - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", config.PreviewTimestamp); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", config.ReleaseId); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", "different_hash"); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_NullCurrentTimestamp_NonNullFingerprint_ReturnsFalse() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithReleaseId("test_release_123") - .WithPreviewResponse() - .Build(); - - config.PreviewTimestamp = null; - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", "2024-11-29T14:30:00.000Z"); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", config.ReleaseId); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_NonNullCurrentTimestamp_NullFingerprint_ReturnsFalse() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("test_release_123") - .WithPreviewResponse() - .Build(); - - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", null); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", config.ReleaseId); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_NullCurrentReleaseId_NonNullFingerprint_ReturnsFalse() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithPreviewResponse() - .Build(); - - config.ReleaseId = null; - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", config.PreviewTimestamp); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", "fingerprint_release"); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_PartialMatch_TimestampAndReleaseMatch_LivePreviewDifferent_ReturnsFalse() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("test_release_123") - .WithLivePreview("current_hash") - .WithPreviewResponse() - .Build(); - - // Set matching fingerprints for timestamp and release, but different for live preview - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", config.PreviewTimestamp); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", config.ReleaseId); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", "different_hash"); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(isCached); - } - - #endregion - - #region Edge Cases - - [Fact] - public void IsCachedPreviewForCurrentQuery_EmptyStringVsNull_ReturnsFalse() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewResponse() - .Build(); - - // Current values are empty strings, fingerprints are null - config.PreviewTimestamp = ""; - config.ReleaseId = ""; - SetInternalProperty(config, "LivePreview", ""); - - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", null); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", null); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", null); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(isCached); // Empty string != null - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_NullVsEmptyString_ReturnsFalse() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewResponse() - .Build(); - - // Current values are null, fingerprints are empty strings - config.PreviewTimestamp = null; - config.ReleaseId = null; - SetInternalProperty(config, "LivePreview", null); - - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", ""); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", ""); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", ""); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(isCached); // null != empty string - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_CaseSensitive_ReturnsFalse() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("Test_Release_123") - .WithLivePreview("Test_Hash") - .WithPreviewResponse() - .Build(); - - // Set fingerprints with different casing - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", config.PreviewTimestamp); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", "test_release_123"); // Different case - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", "test_hash"); // Different case - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(isCached); // Should be case-sensitive - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_WhitespaceSignificant_ReturnsFalse() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithPreviewResponse() - .Build(); - - // Set fingerprint with extra whitespace - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", " 2024-11-29T14:30:00.000Z "); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(isCached); // Whitespace should be significant - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_UnicodeCharacters_MatchesCorrectly() - { - // Arrange - var unicodeTimestamp = "2024-11-29T14:30:00.000Z-üñíçødé"; - var unicodeReleaseId = "release-测试-🚀"; - var unicodeHash = "hash-αβγδε-💯"; - - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp(unicodeTimestamp) - .WithReleaseId(unicodeReleaseId) - .WithLivePreview(unicodeHash) - .WithPreviewResponse() - .Build(); - - // Set matching unicode fingerprints - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", unicodeTimestamp); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", unicodeReleaseId); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", unicodeHash); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.True(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_VeryLongStrings_HandlesCorrectly() - { - // Arrange - var longTimestamp = "2024-11-29T14:30:00.000Z" + new string('x', 1000); - var longReleaseId = "release_" + new string('y', 1000); - var longHash = "hash_" + new string('z', 1000); - - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp(longTimestamp) - .WithReleaseId(longReleaseId) - .WithLivePreview(longHash) - .WithPreviewResponse() - .Build(); - - // Set matching long fingerprints - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", longTimestamp); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", longReleaseId); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", longHash); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.True(isCached); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_SpecialCharacters_HandlesCorrectly() - { - // Arrange - var specialTimestamp = "2024-11-29T14:30:00.000Z!@#$%^&*()"; - var specialReleaseId = "release_<>&\"'`~"; - var specialHash = "hash_{[]}|\\:;?,./"; - - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp(specialTimestamp) - .WithReleaseId(specialReleaseId) - .WithLivePreview(specialHash) - .WithPreviewResponse() - .Build(); - - // Set matching special character fingerprints - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", specialTimestamp); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", specialReleaseId); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", specialHash); - - // Act - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.True(isCached); - } - - #endregion - - #region Performance - - [Fact] - public void IsCachedPreviewForCurrentQuery_Performance_FastComparison() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("performance_test_release") - .WithLivePreview("performance_test_hash") - .WithPreviewResponse() - .WithMatchingFingerprints() - .Build(); - - var iterations = 10000; - var startTime = DateTime.UtcNow; - - // Act - Multiple cache checks - for (int i = 0; i < iterations; i++) - { - var result = config.IsCachedPreviewForCurrentQuery(); - Assert.True(result); // Verify correctness during performance test - } - - var duration = DateTime.UtcNow - startTime; - - // Assert - Should be very fast (under 50ms for 10,000 checks) - Assert.True(duration.TotalMilliseconds < 50, - $"Cache check took {duration.TotalMilliseconds}ms for {iterations} operations"); - } - - #endregion - } -} \ No newline at end of file diff --git a/Contentstack.Core.Unit.Tests/LivePreviewConfigUnitTests.cs b/Contentstack.Core.Unit.Tests/LivePreviewConfigUnitTests.cs index bd6a7b88..cef349fb 100644 --- a/Contentstack.Core.Unit.Tests/LivePreviewConfigUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/LivePreviewConfigUnitTests.cs @@ -1,8 +1,6 @@ using System; -using System.Reflection; using AutoFixture; using Contentstack.Core.Configuration; -using Newtonsoft.Json.Linq; using Xunit; namespace Contentstack.Core.Unit.Tests @@ -153,309 +151,6 @@ public void LivePreviewConfig_WithAllPropertiesSet_ReturnsAllValues() } #endregion - - #region Timeline Preview Property Tests - - [Fact] - public void PreviewResponse_SetAndGet_ReturnsCorrectValue() - { - // Arrange - var config = new LivePreviewConfig(); - var response = JObject.Parse(@"{ - ""entry"": { - ""uid"": ""test_entry"", - ""title"": ""Test Entry"" - } - }"); - - // Act - config.PreviewResponse = response; - - // Assert - Assert.Same(response, config.PreviewResponse); - } - - [Fact] - public void PreviewResponse_SetNull_ReturnsNull() - { - // Arrange - var config = new LivePreviewConfig(); - config.PreviewResponse = JObject.Parse(@"{ ""test"": ""value"" }"); - - // Act - config.PreviewResponse = null; - - // Assert - Assert.Null(config.PreviewResponse); - } - - #endregion - - #region Fingerprint Property Tests - - [Fact] - public void PreviewResponseFingerprintPreviewTimestamp_SetAndGet_InternalProperty() - { - // Arrange - var config = new LivePreviewConfig(); - var timestamp = "2024-11-29T14:30:00.000Z"; - - // Act - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", timestamp); - var result = GetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp"); - - // Assert - Assert.Equal(timestamp, result); - } - - [Fact] - public void PreviewResponseFingerprintReleaseId_SetAndGet_InternalProperty() - { - // Arrange - var config = new LivePreviewConfig(); - var releaseId = _fixture.Create(); - - // Act - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", releaseId); - var result = GetInternalProperty(config, "PreviewResponseFingerprintReleaseId"); - - // Assert - Assert.Equal(releaseId, result); - } - - [Fact] - public void PreviewResponseFingerprintLivePreview_SetAndGet_InternalProperty() - { - // Arrange - var config = new LivePreviewConfig(); - var hash = _fixture.Create(); - - // Act - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", hash); - var result = GetInternalProperty(config, "PreviewResponseFingerprintLivePreview"); - - // Assert - Assert.Equal(hash, result); - } - - [Fact] - public void ContentTypeUID_SetAndGet_InternalProperty() - { - // Arrange - var config = new LivePreviewConfig(); - var contentTypeUid = _fixture.Create(); - - // Act - SetInternalProperty(config, "ContentTypeUID", contentTypeUid); - var result = GetInternalProperty(config, "ContentTypeUID"); - - // Assert - Assert.Equal(contentTypeUid, result); - } - - [Fact] - public void EntryUID_SetAndGet_InternalProperty() - { - // Arrange - var config = new LivePreviewConfig(); - var entryUid = _fixture.Create(); - - // Act - SetInternalProperty(config, "EntryUID", entryUid); - var result = GetInternalProperty(config, "EntryUID"); - - // Assert - Assert.Equal(entryUid, result); - } - - [Fact] - public void LivePreview_SetAndGet_InternalProperty() - { - // Arrange - var config = new LivePreviewConfig(); - var hash = _fixture.Create(); - - // Act - SetInternalProperty(config, "LivePreview", hash); - var result = GetInternalProperty(config, "LivePreview"); - - // Assert - Assert.Equal(hash, result); - } - - #endregion - - #region Basic Cache Method Tests - - [Fact] - public void IsCachedPreviewForCurrentQuery_NullPreviewResponse_ReturnsFalse() - { - // Arrange - var config = new LivePreviewConfig - { - PreviewTimestamp = "2024-11-29T14:30:00.000Z", - ReleaseId = "test_release" - }; - config.PreviewResponse = null; - - // Act - var result = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(result); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_AllValuesNull_WithPreviewResponse_ReturnsTrue() - { - // Arrange - var config = new LivePreviewConfig(); - config.PreviewResponse = JObject.Parse(@"{ ""entry"": { ""uid"": ""test"" } }"); - - // Ensure all values and fingerprints are null - config.PreviewTimestamp = null; - config.ReleaseId = null; - SetInternalProperty(config, "LivePreview", null); - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", null); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", null); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", null); - - // Act - var result = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.True(result); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_SingleMatchingProperty_ReturnsTrue() - { - // Arrange - var config = new LivePreviewConfig - { - PreviewTimestamp = "2024-11-29T14:30:00.000Z" - }; - config.PreviewResponse = JObject.Parse(@"{ ""entry"": { ""uid"": ""test"" } }"); - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", config.PreviewTimestamp); - - // Act - var result = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.True(result); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_SingleNonMatchingProperty_ReturnsFalse() - { - // Arrange - var config = new LivePreviewConfig - { - PreviewTimestamp = "2024-11-29T14:30:00.000Z" - }; - config.PreviewResponse = JObject.Parse(@"{ ""entry"": { ""uid"": ""test"" } }"); - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", "2024-11-29T10:00:00.000Z"); - - // Act - var result = config.IsCachedPreviewForCurrentQuery(); - - // Assert - Assert.False(result); - } - - #endregion - - #region Fingerprint Integration Tests - - [Fact] - public void FingerprintProperties_IndependentValues_NoInterference() - { - // Arrange - var config = new LivePreviewConfig(); - var timestamp = "2024-11-29T14:30:00.000Z"; - var releaseId = "test_release_123"; - var hash = "test_hash_456"; - - // Act - Set each fingerprint independently - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", timestamp); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", releaseId); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", hash); - - // Assert - Each maintains its own value - Assert.Equal(timestamp, GetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp")); - Assert.Equal(releaseId, GetInternalProperty(config, "PreviewResponseFingerprintReleaseId")); - Assert.Equal(hash, GetInternalProperty(config, "PreviewResponseFingerprintLivePreview")); - - // Modify one - others should be unaffected - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", "modified_timestamp"); - - Assert.Equal("modified_timestamp", GetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp")); - Assert.Equal(releaseId, GetInternalProperty(config, "PreviewResponseFingerprintReleaseId")); - Assert.Equal(hash, GetInternalProperty(config, "PreviewResponseFingerprintLivePreview")); - } - - [Fact] - public void FingerprintProperties_NullValues_HandledCorrectly() - { - // Arrange - var config = new LivePreviewConfig(); - - // Act - Set to null - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", null); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", null); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", null); - - // Assert - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp")); - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintReleaseId")); - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintLivePreview")); - } - - [Fact] - public void ContextProperties_IndependentValues_NoInterference() - { - // Arrange - var config = new LivePreviewConfig(); - var contentTypeUid = "test_ct_123"; - var entryUid = "test_entry_456"; - var livePreview = "test_hash_789"; - - // Act - SetInternalProperty(config, "ContentTypeUID", contentTypeUid); - SetInternalProperty(config, "EntryUID", entryUid); - SetInternalProperty(config, "LivePreview", livePreview); - - // Assert - Assert.Equal(contentTypeUid, GetInternalProperty(config, "ContentTypeUID")); - Assert.Equal(entryUid, GetInternalProperty(config, "EntryUID")); - Assert.Equal(livePreview, GetInternalProperty(config, "LivePreview")); - } - - #endregion - - #region Helper Methods - - /// - /// Sets internal property value using reflection (for testing internal state) - /// - private void SetInternalProperty(object target, string propertyName, object value) - { - var property = target.GetType().GetProperty(propertyName, - BindingFlags.NonPublic | BindingFlags.Instance); - property?.SetValue(target, value); - } - - /// - /// Gets internal property value using reflection (for testing internal state) - /// - private T GetInternalProperty(object target, string propertyName) - { - var property = target.GetType().GetProperty(propertyName, - BindingFlags.NonPublic | BindingFlags.Instance); - return (T)property?.GetValue(target); - } - - #endregion } } diff --git a/Contentstack.Core.Unit.Tests/Mokes/MockHttpHandler.cs b/Contentstack.Core.Unit.Tests/Mokes/MockHttpHandler.cs index e1d50e38..57193312 100644 --- a/Contentstack.Core.Unit.Tests/Mokes/MockHttpHandler.cs +++ b/Contentstack.Core.Unit.Tests/Mokes/MockHttpHandler.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using Contentstack.Core.Interfaces; +using System.Text.Json.Nodes; namespace Contentstack.Core.Unit.Tests.Mokes { @@ -80,9 +81,9 @@ public string OpenResponse() return _response; } - public Newtonsoft.Json.Linq.JObject OpenJObjectResponse() + public JsonObject OpenJsonObjectResponse() { - return Newtonsoft.Json.Linq.JObject.Parse(_response ?? "{}"); + return JsonNode.Parse(_response ?? "{}").AsObject(); } } } diff --git a/Contentstack.Core.Unit.Tests/Mokes/MockInfrastructureTest.cs b/Contentstack.Core.Unit.Tests/Mokes/MockInfrastructureTest.cs index 263dd7da..54e47a16 100644 --- a/Contentstack.Core.Unit.Tests/Mokes/MockInfrastructureTest.cs +++ b/Contentstack.Core.Unit.Tests/Mokes/MockInfrastructureTest.cs @@ -27,13 +27,13 @@ public void MockResponse_CreateContentstackResponse_ShouldLoadFromResource() } [Fact] - public void MockResponse_CreateContentstackResponseAsJObject_ShouldParseJson() + public void MockResponse_CreateContentstackResponseAsJsonObject_ShouldParseJson() { // Arrange & Act - var jObject = MockResponse.CreateContentstackResponseAsJObject("MockResponse.txt"); + var jsonObject = MockResponse.CreateContentstackResponseAsJsonObject("MockResponse.txt"); // Assert - Assert.NotNull(jObject); + Assert.NotNull(jsonObject); } [Fact] @@ -51,18 +51,18 @@ public void ContentstackResponse_OpenResponse_ShouldReturnResponseString() } [Fact] - public void ContentstackResponse_OpenJObjectResponse_ShouldParseJson() + public void ContentstackResponse_OpenJsonObjectResponse_ShouldParseJson() { // Arrange var responseString = "{\"test\": \"value\"}"; var response = new ContentstackResponse(responseString); // Act - var jObject = response.OpenJObjectResponse(); + var jsonObject = response.OpenJsonObjectResponse(); // Assert - Assert.NotNull(jObject); - Assert.Equal("value", jObject["test"]?.ToString()); + Assert.NotNull(jsonObject); + Assert.Equal("value", jsonObject["test"]?.GetValue()); } [Fact] diff --git a/Contentstack.Core.Unit.Tests/Mokes/MockResponse.cs b/Contentstack.Core.Unit.Tests/Mokes/MockResponse.cs index ce2f3517..967ed2eb 100644 --- a/Contentstack.Core.Unit.Tests/Mokes/MockResponse.cs +++ b/Contentstack.Core.Unit.Tests/Mokes/MockResponse.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Reflection; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; using Contentstack.Core.Unit.Tests.Mokes; namespace Contentstack.Core.Unit.Tests.Mokes @@ -58,14 +58,14 @@ public static string CreateContentstackResponse(string resourceName) } /// - /// Creates a JObject from a mock response file + /// Creates a from a mock response file /// /// Name of the resource file - /// JObject parsed from the response - public static JObject CreateContentstackResponseAsJObject(string resourceName) + /// JSON object parsed from the response + public static JsonObject CreateContentstackResponseAsJsonObject(string resourceName) { var jsonString = CreateContentstackResponse(resourceName); - return JObject.Parse(jsonString); + return JsonNode.Parse(jsonString).AsObject(); } /// diff --git a/Contentstack.Core.Unit.Tests/Mokes/TimelineMockHttpHandler.cs b/Contentstack.Core.Unit.Tests/Mokes/TimelineMockHttpHandler.cs deleted file mode 100644 index b5790591..00000000 --- a/Contentstack.Core.Unit.Tests/Mokes/TimelineMockHttpHandler.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Contentstack.Core.Interfaces; -using Newtonsoft.Json.Linq; - -namespace Contentstack.Core.Unit.Tests.Mokes -{ - /// - /// Mock HTTP handler specialized for Timeline Preview functionality testing - /// - public class TimelineMockHttpHandler : IContentstackPlugin - { - private TimeSpan _delay = TimeSpan.Zero; - private bool _shouldThrowTimeout = false; - private bool _shouldThrowWebException = false; - private string _webExceptionMessage = "Network error"; - private JObject _mockResponse; - private string _expectedEntryUid; - private string _expectedContentTypeUid; - - /// - /// List of requests that have been processed by this handler - /// - public List Requests { get; private set; } = new List(); - - /// - /// Configure handler to return successful live preview responses - /// - /// Expected entry UID in requests - /// Expected content type UID in requests - /// Handler instance for chaining - public TimelineMockHttpHandler ForSuccessfulLivePreview(string entryUid = "test_entry", string contentTypeUid = "test_ct") - { - _expectedEntryUid = entryUid; - _expectedContentTypeUid = contentTypeUid; - - _mockResponse = JObject.Parse($@"{{ - ""entry"": {{ - ""uid"": ""{entryUid}"", - ""content_type_uid"": ""{contentTypeUid}"", - ""title"": ""Timeline Mock Entry"", - ""created_at"": ""2024-11-29T10:00:00.000Z"", - ""updated_at"": ""2024-11-29T14:30:00.000Z"", - ""publish_details"": {{ - ""environment"": ""test"", - ""locale"": ""en-us"", - ""time"": ""2024-11-29T14:30:00.000Z"" - }}, - ""mock_field"": ""timeline_test_value"", - ""_metadata"": {{ - ""mock_handler"": true, - ""timestamp"": ""{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ}"" - }} - }} - }}"); - - return this; - } - - /// - /// Configure handler to return a custom live preview response - /// - /// Custom JObject response - /// Handler instance for chaining - public TimelineMockHttpHandler ForLivePreview(JObject response) - { - _mockResponse = response; - return this; - } - - /// - /// Configure handler to return timeline-specific response for different timestamps - /// - /// Timeline timestamp - /// Release ID - /// Entry UID - /// Content Type UID - /// Handler instance for chaining - public TimelineMockHttpHandler ForTimelineScenario(string timestamp, string releaseId = null, string entryUid = "test_entry", string contentTypeUid = "test_ct") - { - _expectedEntryUid = entryUid; - _expectedContentTypeUid = contentTypeUid; - - _mockResponse = JObject.Parse($@"{{ - ""entry"": {{ - ""uid"": ""{entryUid}"", - ""content_type_uid"": ""{contentTypeUid}"", - ""title"": ""Timeline Entry at {timestamp}"", - ""created_at"": ""2024-11-29T10:00:00.000Z"", - ""updated_at"": ""{timestamp}"", - ""publish_details"": {{ - ""environment"": ""test"", - ""locale"": ""en-us"", - ""time"": ""{timestamp}"", - ""release_id"": ""{releaseId ?? "default_release"}"" - }}, - ""timeline_content"": ""Content for {timestamp}"", - ""_metadata"": {{ - ""timeline_timestamp"": ""{timestamp}"", - ""timeline_release"": ""{releaseId ?? "default_release"}"", - ""mock_handler"": true - }} - }} - }}"); - - return this; - } - - /// - /// Configure handler to simulate network timeout - /// - /// Handler instance for chaining - public TimelineMockHttpHandler ThrowTimeout() - { - _shouldThrowTimeout = true; - return this; - } - - /// - /// Configure handler to simulate network error - /// - /// Error message - /// Handler instance for chaining - public TimelineMockHttpHandler ThrowWebException(string message = "Network error") - { - _shouldThrowWebException = true; - _webExceptionMessage = message; - return this; - } - - /// - /// Add artificial delay to simulate network latency - /// - /// Delay duration - /// Handler instance for chaining - public TimelineMockHttpHandler WithDelay(TimeSpan delay) - { - _delay = delay; - return this; - } - - /// - /// Handle HTTP request processing (IContentstackPlugin implementation) - /// - /// Contentstack client - /// HTTP request - /// Modified request - public async Task OnRequest(ContentstackClient stack, HttpWebRequest request) - { - // Track the request for testing purposes - Requests.Add(request); - - // Simulate delay if configured - if (_delay > TimeSpan.Zero) - { - await Task.Delay(_delay); - } - - // Simulate timeout if configured - if (_shouldThrowTimeout) - { - throw new TaskCanceledException("Request timeout"); - } - - // Just pass through the request (no modifications needed for our mock) - return await Task.FromResult(request); - } - - /// - /// Handle HTTP response processing (IContentstackPlugin implementation) - /// - /// Contentstack client - /// HTTP request - /// HTTP response - /// Original response string - /// Mock response string - public async Task OnResponse(ContentstackClient stack, HttpWebRequest request, HttpWebResponse response, string responseString) - { - // Simulate web exception if configured - if (_shouldThrowWebException) - { - throw new WebException(_webExceptionMessage); - } - - // Return mock response instead of actual response - if (_mockResponse != null) - { - return await Task.FromResult(_mockResponse.ToString()); - } - - // Default fallback response - return await Task.FromResult(CreateDefaultResponse()); - } - - /// - /// Create multiple timeline responses for comparison testing - /// - /// Array of timestamps - /// Entry UID - /// Content Type UID - /// Handler configured for multiple scenarios - public TimelineMockHttpHandler ForMultipleTimelines(string[] timestamps, string entryUid = "test_entry", string contentTypeUid = "test_ct") - { - // For simplicity, this returns the first timestamp scenario - // In a more sophisticated implementation, this could track request parameters - // and return different responses based on the request context - if (timestamps != null && timestamps.Length > 0) - { - return ForTimelineScenario(timestamps[0], null, entryUid, contentTypeUid); - } - - return ForSuccessfulLivePreview(entryUid, contentTypeUid); - } - - /// - /// Configure for cache testing scenarios - /// - /// Unique fingerprint for this response - /// Handler instance for chaining - public TimelineMockHttpHandler ForCacheScenario(string fingerprint) - { - _mockResponse = JObject.Parse($@"{{ - ""entry"": {{ - ""uid"": ""cache_test_entry"", - ""content_type_uid"": ""cache_test_ct"", - ""title"": ""Cache Test Entry"", - ""cache_fingerprint"": ""{fingerprint}"", - ""created_at"": ""2024-11-29T10:00:00.000Z"", - ""updated_at"": ""2024-11-29T14:30:00.000Z"", - ""_metadata"": {{ - ""cache_test"": true, - ""fingerprint"": ""{fingerprint}"" - }} - }} - }}"); - - return this; - } - - private string CreateDefaultResponse() - { - return JObject.Parse(@"{ - ""entry"": { - ""uid"": ""default_entry"", - ""content_type_uid"": ""default_ct"", - ""title"": ""Default Mock Entry"", - ""created_at"": ""2024-11-29T10:00:00.000Z"", - ""updated_at"": ""2024-11-29T14:30:00.000Z"", - ""_metadata"": { - ""default_response"": true - } - } - }").ToString(); - } - } -} \ No newline at end of file diff --git a/Contentstack.Core.Unit.Tests/SyncStackUnitTests.cs b/Contentstack.Core.Unit.Tests/SyncStackUnitTests.cs index 888902d8..f84bb099 100644 --- a/Contentstack.Core.Unit.Tests/SyncStackUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/SyncStackUnitTests.cs @@ -282,7 +282,7 @@ public void SyncStack_Items_CanBeIterated() #endregion - #region JSON Serialization Tests (Based on JsonProperty attributes) + #region JSON Serialization Tests (Based on JsonPropertyName attributes) [Fact] public void SyncStack_TotalCount_ShouldMapToTotalCountProperty() @@ -294,7 +294,7 @@ public void SyncStack_TotalCount_ShouldMapToTotalCountProperty() }; // Act & Assert - // The property uses [JsonProperty("total_count")] so it should serialize correctly + // The property uses [JsonPropertyName("total_count")] so it should serialize correctly Assert.Equal(42, syncStack.TotalCount); } @@ -308,7 +308,7 @@ public void SyncStack_SyncToken_ShouldMapToSyncTokenProperty() }; // Act & Assert - // The property uses [JsonProperty("sync_token")] so it should serialize correctly + // The property uses [JsonPropertyName("sync_token")] so it should serialize correctly Assert.Equal("test_token", syncStack.SyncToken); } @@ -322,7 +322,7 @@ public void SyncStack_PaginationToken_ShouldMapToPaginationTokenProperty() }; // Act & Assert - // The property uses [JsonProperty("pagination_token")] so it should serialize correctly + // The property uses [JsonPropertyName("pagination_token")] so it should serialize correctly Assert.Equal("test_pagination_token", syncStack.PaginationToken); } diff --git a/Contentstack.Core.Unit.Tests/TimelineCacheBehaviorTests.cs b/Contentstack.Core.Unit.Tests/TimelineCacheBehaviorTests.cs deleted file mode 100644 index 8b030c48..00000000 --- a/Contentstack.Core.Unit.Tests/TimelineCacheBehaviorTests.cs +++ /dev/null @@ -1,561 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoFixture; -using Contentstack.Core.Configuration; -using Contentstack.Core.Models; -using Contentstack.Core.Unit.Tests.Helpers; -using Contentstack.Core.Unit.Tests.Mokes; -using Newtonsoft.Json.Linq; -using Xunit; - -namespace Contentstack.Core.Unit.Tests -{ - /// - /// Unit tests for Timeline Preview cache behavior and performance - /// Tests fingerprint-based caching, cache hits/misses, and performance optimizations - /// - [Trait("Category", "TimelinePreview")] - [Trait("Category", "Cache")] - [Trait("Category", "Performance")] - public class TimelineCacheBehaviorTests : ContentstackClientTestBase - { - #region Cache Hit Performance - - [Fact] - public async Task CacheHit_SignificantlyFasterThanNetworkRequest() - { - // Arrange - var client = CreateClientWithLivePreview(); - var config = client.GetLivePreviewConfig(); - - // Set up cached response - var cachedResponse = TimelineTestDataBuilder.New() - .CreateValidPreviewResponse("perf_entry", "perf_ct"); - - config.PreviewResponse = cachedResponse; - config.PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - config.ReleaseId = "perf_release"; - - SetInternalProperty(config, "ContentTypeUID", "perf_ct"); - SetInternalProperty(config, "EntryUID", "perf_entry"); - - // Set matching fingerprints for cache hit - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", config.PreviewTimestamp); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", config.ReleaseId); - - // Create query that should hit cache - var cacheHitQuery = CreateLivePreviewQuery( - contentTypeUid: "perf_ct", - entryUid: "perf_entry", - previewTimestamp: "2024-11-29T14:30:00.000Z", - releaseId: "perf_release" - ); - - // Act & Assert - Cache operation should be fast - await TimelinePerformanceHelpers.AssertCachePerformance( - cacheOperation: async () => - { - // This should hit cache - test the cache checking logic directly - var isCached = config.IsCachedPreviewForCurrentQuery(); - Assert.True(isCached); - - // Access cached response - var result = config.PreviewResponse; - Assert.NotNull(result); - - // Small delay to simulate cache access overhead - await Task.Delay(1); - }, - networkOperation: async () => - { - // Simulate realistic network delay - await Task.Delay(50); - var result = CreateMockPreviewResponse(); - Assert.NotNull(result); - }, - description: "Timeline Preview Cache Hit", - minimumSpeedupFactor: 3 - ); - } - - [Fact] - public void CacheHit_MultipleRequests_ConsistentPerformance() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("consistent_release") - .WithPreviewResponse() - .WithMatchingFingerprints() - .Build(); - - var iterations = 1000; - - // Act & Assert - Multiple cache checks should be consistently fast - TimelinePerformanceHelpers.AssertExecutionTime(() => - { - for (int i = 0; i < iterations; i++) - { - var isCached = config.IsCachedPreviewForCurrentQuery(); - Assert.True(isCached); - } - }, TimeSpan.FromMilliseconds(10), $"{iterations} cache hit checks"); - } - - [Fact] - public void CacheHit_MemoryEfficient_NoLeaks() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithPreviewResponse() - .WithMatchingFingerprints() - .Build(); - - // Act & Assert - Multiple cache hits should not leak memory - TimelinePerformanceHelpers.AssertNoMemoryLeak(() => - { - var isCached = config.IsCachedPreviewForCurrentQuery(); - Assert.True(isCached); - }, iterations: 10000, maxMemoryGrowth: 1024 * 1024); // 1MB max growth - } - - #endregion - - #region Cache Miss Behavior - - [Fact] - public void CacheMiss_FingerprintMismatch_CorrectDetection() - { - // Arrange - Set up cache miss scenarios - var scenarios = new[] - { - // Timestamp mismatch - TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithPreviewResponse() - .Build(), - - // Release ID mismatch - TimelineTestDataBuilder.New() - .WithReleaseId("current_release") - .WithPreviewResponse() - .Build(), - - // Live preview hash mismatch - TimelineTestDataBuilder.New() - .WithLivePreview("current_hash") - .WithPreviewResponse() - .Build() - }; - - // Set mismatched fingerprints - SetInternalProperty(scenarios[0], "PreviewResponseFingerprintPreviewTimestamp", "2024-11-29T10:00:00.000Z"); - SetInternalProperty(scenarios[1], "PreviewResponseFingerprintReleaseId", "old_release"); - SetInternalProperty(scenarios[2], "PreviewResponseFingerprintLivePreview", "old_hash"); - - // Act & Assert - All should detect cache miss - foreach (var config in scenarios) - { - Assert.False(config.IsCachedPreviewForCurrentQuery()); - } - } - - [Fact] - public void CacheMiss_NullResponse_AlwaysMiss() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("test_release") - .WithMatchingFingerprints() - .Build(); - - config.PreviewResponse = null; - - // Act & Assert - Null response should always be cache miss - Assert.False(config.IsCachedPreviewForCurrentQuery()); - } - - [Fact] - public void CacheMiss_PartialFingerprints_CorrectBehavior() - { - // Arrange - Test partial fingerprint scenarios - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("test_release") - .WithLivePreview("test_hash") - .WithPreviewResponse() - .Build(); - - var testCases = new[] - { - new { Description = "Only timestamp fingerprint set", SetTimestamp = true, SetRelease = false, SetHash = false }, - new { Description = "Only release fingerprint set", SetTimestamp = false, SetRelease = true, SetHash = false }, - new { Description = "Only hash fingerprint set", SetTimestamp = false, SetRelease = false, SetHash = true }, - new { Description = "Timestamp and release fingerprints set", SetTimestamp = true, SetRelease = true, SetHash = false }, - new { Description = "Timestamp and hash fingerprints set", SetTimestamp = true, SetRelease = false, SetHash = true }, - new { Description = "Release and hash fingerprints set", SetTimestamp = false, SetRelease = true, SetHash = true } - }; - - foreach (var testCase in testCases) - { - // Set fingerprints based on test case - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", - testCase.SetTimestamp ? config.PreviewTimestamp : null); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", - testCase.SetRelease ? config.ReleaseId : null); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", - testCase.SetHash ? GetInternalProperty(config, "LivePreview") : null); - - // Act & Assert - var isCached = config.IsCachedPreviewForCurrentQuery(); - - // Should only be cache hit if ALL non-null current values match their fingerprints - bool expectedCacheHit = true; - if (!string.IsNullOrEmpty(config.PreviewTimestamp) && !testCase.SetTimestamp) expectedCacheHit = false; - if (!string.IsNullOrEmpty(config.ReleaseId) && !testCase.SetRelease) expectedCacheHit = false; - if (!string.IsNullOrEmpty(GetInternalProperty(config, "LivePreview")) && !testCase.SetHash) expectedCacheHit = false; - - Assert.True(expectedCacheHit == isCached, testCase.Description); - } - } - - #endregion - - #region Cache Fingerprint Updates - - [Fact] - public async Task CacheFingerprint_UpdatedAfterSuccessfulQuery() - { - // Arrange - var client = CreateClientWithLivePreview(); - var mockHandler = new TimelineMockHttpHandler() - .ForLivePreview(JObject.Parse(TimelineMockHelpers.CreateMockLivePreviewResponse())) - .WithDelay(TimeSpan.FromMilliseconds(50)); // Predictable delay - client.Plugins.Add(mockHandler); - - var query = CreateLivePreviewQuery( - contentTypeUid: "fingerprint_ct", - entryUid: "fingerprint_entry", - previewTimestamp: "2024-11-29T14:30:00.000Z", - releaseId: "test_release" - ); - - var config = client.GetLivePreviewConfig(); - - // Verify initial state (no fingerprints) - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp")); - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintReleaseId")); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Verify that config values are properly set from the query - Assert.Equal("2024-11-29T14:30:00.000Z", config.PreviewTimestamp); - Assert.Equal("test_release", config.ReleaseId); - Assert.Equal("fingerprint_ct", config.ContentTypeUID); - Assert.Equal("fingerprint_entry", config.EntryUID); - - // Verify that a mock request was made - Assert.True(mockHandler.Requests.Count > 0); - } - - [Fact] - public void CacheFingerprint_ResetClearsFingerprints() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Set fingerprints - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", "2024-11-29T14:30:00.000Z"); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", "test_release"); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", "test_hash"); - - // Act - client.ResetLivePreview(); - - // Assert - Fingerprints should be cleared - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp")); - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintReleaseId")); - Assert.Null(GetInternalProperty(config, "PreviewResponseFingerprintLivePreview")); - } - - [Fact] - public void CacheFingerprint_ForkPreservesFingerprints() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var parentConfig = parentClient.GetLivePreviewConfig(); - - // Set parent fingerprints - SetInternalProperty(parentConfig, "PreviewResponseFingerprintPreviewTimestamp", "2024-11-29T14:30:00.000Z"); - SetInternalProperty(parentConfig, "PreviewResponseFingerprintReleaseId", "parent_release"); - SetInternalProperty(parentConfig, "PreviewResponseFingerprintLivePreview", "parent_hash"); - - // Act - var forkedClient = parentClient.Fork(); - var forkedConfig = forkedClient.GetLivePreviewConfig(); - - // Assert - Fingerprints should be preserved in fork - Assert.Equal("2024-11-29T14:30:00.000Z", - GetInternalProperty(forkedConfig, "PreviewResponseFingerprintPreviewTimestamp")); - Assert.Equal("parent_release", - GetInternalProperty(forkedConfig, "PreviewResponseFingerprintReleaseId")); - Assert.Equal("parent_hash", - GetInternalProperty(forkedConfig, "PreviewResponseFingerprintLivePreview")); - - // But they should be independent instances - SetInternalProperty(forkedConfig, "PreviewResponseFingerprintPreviewTimestamp", "2024-11-30T00:00:00.000Z"); - - // Parent should be unchanged - Assert.Equal("2024-11-29T14:30:00.000Z", - GetInternalProperty(parentConfig, "PreviewResponseFingerprintPreviewTimestamp")); - } - - #endregion - - #region Cache Performance Optimization - - [Fact] - public void Cache_OptimizedStringComparison_Performance() - { - // Arrange - var longTimestamp = "2024-11-29T14:30:00.000Z" + new string('x', 1000); - var longReleaseId = "release_" + new string('y', 1000); - var longHash = "hash_" + new string('z', 1000); - - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp(longTimestamp) - .WithReleaseId(longReleaseId) - .WithLivePreview(longHash) - .WithPreviewResponse() - .Build(); - - // Set matching fingerprints - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", longTimestamp); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", longReleaseId); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", longHash); - - var iterations = 1000; - - // Act & Assert - Even with long strings, should be fast - TimelinePerformanceHelpers.AssertExecutionTime(() => - { - for (int i = 0; i < iterations; i++) - { - var isCached = config.IsCachedPreviewForCurrentQuery(); - Assert.True(isCached); - } - }, TimeSpan.FromMilliseconds(50), $"{iterations} long string comparisons"); - } - - [Fact] - public void Cache_ConcurrentAccess_ThreadSafe() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithReleaseId("concurrent_release") - .WithPreviewResponse() - .WithMatchingFingerprints() - .Build(); - - var numberOfThreads = 10; - var operationsPerThread = 100; - var allResults = new List(); - var lockObject = new object(); - - // Act - Concurrent cache checks - var tasks = new Task[numberOfThreads]; - for (int threadId = 0; threadId < numberOfThreads; threadId++) - { - tasks[threadId] = Task.Run(() => - { - var threadResults = new List(); - for (int i = 0; i < operationsPerThread; i++) - { - var isCached = config.IsCachedPreviewForCurrentQuery(); - threadResults.Add(isCached); - } - - lock (lockObject) - { - allResults.AddRange(threadResults); - } - }); - } - - Task.WaitAll(tasks); - - // Assert - All operations should return consistent results - var expectedResults = numberOfThreads * operationsPerThread; - Assert.Equal(expectedResults, allResults.Count); - Assert.All(allResults, result => Assert.True(result)); - } - - #endregion - - #region Entry.Fetch Integration - - [Fact] - public async Task EntryFetch_CacheHit_SkipsNetworkRequest() - { - // Arrange - var client = CreateClientWithLivePreview(); - var config = client.GetLivePreviewConfig(); - - // Set up cached response - var cachedResponse = TimelineTestDataBuilder.New() - .CreateValidPreviewResponse("fetch_entry", "fetch_ct"); - - config.PreviewResponse = cachedResponse; - config.PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - - SetInternalProperty(config, "ContentTypeUID", "fetch_ct"); - SetInternalProperty(config, "EntryUID", "fetch_entry"); - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", config.PreviewTimestamp); - - var contentType = client.ContentType("fetch_ct"); - var entry = contentType.Entry("fetch_entry"); - - var mockHandler = new TimelineMockHttpHandler() - .ForSuccessfulLivePreview("fetch_entry", "fetch_ct"); - - // Don't add handler to client - if cache works, no network call should be made - - // Act & Assert - Should use cache, not make network request - var result = await entry.Fetch(); - - Assert.NotNull(result); - // Should return the cached response data - Assert.Equal("fetch_entry", result["uid"]?.ToString()); - } - - [Fact] - public async Task LivePreviewQuery_CacheMiss_AttemptsNetworkRequest() - { - // Arrange - var client = CreateClientWithLivePreview(); - var config = client.GetLivePreviewConfig(); - - // Set up cache miss scenario - set non-matching fingerprints - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", "2024-11-29T10:00:00.000Z"); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", "old_release"); - - var mockHandler = new TimelineMockHttpHandler() - .ForLivePreview(JObject.Parse(TimelineMockHelpers.CreateMockLivePreviewResponse())); - client.Plugins.Add(mockHandler); - - // Create query that will cause cache miss - var query = CreateLivePreviewQuery( - contentTypeUid: "network_ct", - entryUid: "network_entry", - previewTimestamp: "2024-11-29T14:30:00.000Z", - releaseId: "network_release" - ); - - // Act - await client.LivePreviewQueryAsync(query); - - // Assert - Should have attempted network request due to cache miss - Assert.NotEmpty(mockHandler.Requests); - - // Verify cache configuration was updated with new values - Assert.Equal("2024-11-29T14:30:00.000Z", config.PreviewTimestamp); - Assert.Equal("network_release", config.ReleaseId); - Assert.Equal("network_ct", config.ContentTypeUID); - Assert.Equal("network_entry", config.EntryUID); - } - - #endregion - - #region Cache Size and Memory Management - - [Fact] - public void Cache_LargeResponses_MemoryEfficient() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithMatchingFingerprints() - .Build(); - - // Create large response - var largeResponseData = JObject.Parse($@"{{ - ""entry"": {{ - ""uid"": ""large_entry"", - ""title"": ""Large Test Entry"", - ""large_field_1"": ""{new string('a', 10000)}"", - ""large_field_2"": ""{new string('b', 10000)}"", - ""large_field_3"": ""{new string('c', 10000)}"", - ""array_field"": [{string.Join(",", Enumerable.Range(0, 1000).Select(i => $"\"{i}\""))}] - }} - }}"); - - config.PreviewResponse = largeResponseData; - - // Act & Assert - Cache operations should be memory efficient - TimelinePerformanceHelpers.AssertNoMemoryLeak(() => - { - var isCached = config.IsCachedPreviewForCurrentQuery(); - Assert.True(isCached); - }, iterations: 1000, maxMemoryGrowth: 512 * 1024); // 512KB max growth for large object caching - } - - [Fact] - public void Cache_MultipleEntries_IndependentCaching() - { - // Arrange - var baseClient = CreateClientWithLivePreview(); - var numberOfEntries = 50; - var configs = new LivePreviewConfig[numberOfEntries]; - - // Set up different cache states for multiple entries - for (int i = 0; i < numberOfEntries; i++) - { - var fork = baseClient.Fork(); - configs[i] = fork.GetLivePreviewConfig(); - - configs[i].PreviewTimestamp = $"2024-11-{(i % 12) + 1:D2}-01T00:00:00.000Z"; - configs[i].ReleaseId = $"release_{i}"; - configs[i].PreviewResponse = TimelineTestDataBuilder.New() - .CreateValidPreviewResponse($"entry_{i}", $"ct_{i}"); - - SetInternalProperty(configs[i], "ContentTypeUID", $"ct_{i}"); - SetInternalProperty(configs[i], "EntryUID", $"entry_{i}"); - SetInternalProperty(configs[i], "PreviewResponseFingerprintPreviewTimestamp", configs[i].PreviewTimestamp); - SetInternalProperty(configs[i], "PreviewResponseFingerprintReleaseId", configs[i].ReleaseId); - } - - // Act & Assert - Each entry should have independent cache behavior - for (int i = 0; i < numberOfEntries; i++) - { - Assert.True(configs[i].IsCachedPreviewForCurrentQuery(), $"Entry {i} should be cached"); - - // Modify one entry's timestamp - only that entry should become cache miss - var originalTimestamp = configs[i].PreviewTimestamp; - configs[i].PreviewTimestamp = "2024-12-01T00:00:00.000Z"; - Assert.False(configs[i].IsCachedPreviewForCurrentQuery(), $"Entry {i} should be cache miss after timestamp change"); - - // Restore timestamp - configs[i].PreviewTimestamp = originalTimestamp; - Assert.True(configs[i].IsCachedPreviewForCurrentQuery(), $"Entry {i} should be cached again after timestamp restore"); - - // Verify other entries are unaffected - for (int j = 0; j < numberOfEntries; j++) - { - if (j != i) - { - Assert.True(configs[j].IsCachedPreviewForCurrentQuery(), $"Entry {j} should remain cached when entry {i} is modified"); - } - } - } - } - - #endregion - } -} \ No newline at end of file diff --git a/Contentstack.Core.Unit.Tests/TimelineIsolationTests.cs b/Contentstack.Core.Unit.Tests/TimelineIsolationTests.cs deleted file mode 100644 index 235d6976..00000000 --- a/Contentstack.Core.Unit.Tests/TimelineIsolationTests.cs +++ /dev/null @@ -1,540 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using AutoFixture; -using Contentstack.Core.Configuration; -using Contentstack.Core.Unit.Tests.Helpers; -using Contentstack.Core.Unit.Tests.Mokes; -using Newtonsoft.Json.Linq; -using Xunit; - -namespace Contentstack.Core.Unit.Tests -{ - /// - /// Unit tests focused on Timeline Preview isolation behavior - /// Tests fork independence, parallel operations, and state isolation - /// - [Trait("Category", "TimelinePreview")] - [Trait("Category", "Isolation")] - [Trait("Category", "Parallel")] - public class TimelineIsolationTests : ContentstackClientTestBase - { - #region Fork Independence - - [Fact] - public void ForkIsolation_IndependentTimelineContexts() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var fork1 = parentClient.Fork(); - var fork2 = parentClient.Fork(); - var fork3 = parentClient.Fork(); - - // Act - Set different timeline contexts - parentClient.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T08:00:00.000Z"; - parentClient.GetLivePreviewConfig().ReleaseId = "parent_release"; - - fork1.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T10:00:00.000Z"; - fork1.GetLivePreviewConfig().ReleaseId = "fork1_release"; - - fork2.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T12:00:00.000Z"; - fork2.GetLivePreviewConfig().ReleaseId = "fork2_release"; - - fork3.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T14:00:00.000Z"; - fork3.GetLivePreviewConfig().ReleaseId = "fork3_release"; - - // Assert - Each maintains its own timeline context - Assert.Equal("2024-11-29T08:00:00.000Z", parentClient.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("parent_release", parentClient.GetLivePreviewConfig().ReleaseId); - - Assert.Equal("2024-11-29T10:00:00.000Z", fork1.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("fork1_release", fork1.GetLivePreviewConfig().ReleaseId); - - Assert.Equal("2024-11-29T12:00:00.000Z", fork2.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("fork2_release", fork2.GetLivePreviewConfig().ReleaseId); - - Assert.Equal("2024-11-29T14:00:00.000Z", fork3.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("fork3_release", fork3.GetLivePreviewConfig().ReleaseId); - } - - [Fact] - public async Task ForkIsolation_ParallelLivePreviewQueryOperations() - { - // Arrange - var parentClient = CreateClientWithLivePreview(); - var fork1 = parentClient.Fork(); - var fork2 = parentClient.Fork(); - var fork3 = parentClient.Fork(); - - var query1 = CreateLivePreviewQuery( - entryUid: "entry1", - previewTimestamp: "2024-11-29T08:00:00.000Z", - releaseId: "release1" - ); - - var query2 = CreateLivePreviewQuery( - entryUid: "entry2", - previewTimestamp: "2024-11-29T12:00:00.000Z", - releaseId: "release2" - ); - - var query3 = CreateLivePreviewQuery( - entryUid: "entry3", - previewTimestamp: "2024-11-29T16:00:00.000Z", - releaseId: "release3" - ); - - // Act - Parallel operations - var tasks = new[] - { - fork1.LivePreviewQueryAsync(query1), - fork2.LivePreviewQueryAsync(query2), - fork3.LivePreviewQueryAsync(query3) - }; - - await Task.WhenAll(tasks); - - // Assert - Each fork maintains its context without interference - Assert.Equal("2024-11-29T08:00:00.000Z", fork1.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("release1", fork1.GetLivePreviewConfig().ReleaseId); - - Assert.Equal("2024-11-29T12:00:00.000Z", fork2.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("release2", fork2.GetLivePreviewConfig().ReleaseId); - - Assert.Equal("2024-11-29T16:00:00.000Z", fork3.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("release3", fork3.GetLivePreviewConfig().ReleaseId); - - // Parent should remain unaffected - Assert.Null(parentClient.GetLivePreviewConfig().PreviewTimestamp); - Assert.Null(parentClient.GetLivePreviewConfig().ReleaseId); - } - - [Fact] - public void ForkIsolation_IndependentCacheHitMissBehavior() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var fork1 = parentClient.Fork(); - var fork2 = parentClient.Fork(); - - // Set up fork1 with cached data (cache hit scenario) - var fork1Config = fork1.GetLivePreviewConfig(); - fork1Config.PreviewTimestamp = "2024-11-29T10:00:00.000Z"; - fork1Config.ReleaseId = "cached_release"; - fork1Config.PreviewResponse = CreateMockPreviewResponse("entry1", "ct1"); - - SetInternalProperty(fork1Config, "PreviewResponseFingerprintPreviewTimestamp", fork1Config.PreviewTimestamp); - SetInternalProperty(fork1Config, "PreviewResponseFingerprintReleaseId", fork1Config.ReleaseId); - - // Set up fork2 with different data (cache miss scenario) - var fork2Config = fork2.GetLivePreviewConfig(); - fork2Config.PreviewTimestamp = "2024-11-29T14:00:00.000Z"; - fork2Config.ReleaseId = "different_release"; - fork2Config.PreviewResponse = CreateMockPreviewResponse("entry2", "ct2"); - - SetInternalProperty(fork2Config, "PreviewResponseFingerprintPreviewTimestamp", "2024-11-29T08:00:00.000Z"); // Different - SetInternalProperty(fork2Config, "PreviewResponseFingerprintReleaseId", "old_release"); // Different - - // Act & Assert - Assert.True(fork1Config.IsCachedPreviewForCurrentQuery()); // Cache hit - Assert.False(fork2Config.IsCachedPreviewForCurrentQuery()); // Cache miss - - // Verify independence - changes to one don't affect the other - fork1Config.PreviewTimestamp = "2024-11-29T20:00:00.000Z"; - Assert.False(fork1Config.IsCachedPreviewForCurrentQuery()); // Now cache miss for fork1 - Assert.False(fork2Config.IsCachedPreviewForCurrentQuery()); // Still cache miss for fork2 - } - - [Fact] - public void ForkIsolation_ResetLivePreview_DoesNotAffectOtherForks() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var fork1 = parentClient.Fork(); - var fork2 = parentClient.Fork(); - var fork3 = parentClient.Fork(); - - // Set up different states - parentClient.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T08:00:00.000Z"; - fork1.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T10:00:00.000Z"; - fork2.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T12:00:00.000Z"; - fork3.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T14:00:00.000Z"; - - // Act - Reset only fork2 - fork2.ResetLivePreview(); - - // Assert - Only fork2 is affected - Assert.Equal("2024-11-29T08:00:00.000Z", parentClient.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("2024-11-29T10:00:00.000Z", fork1.GetLivePreviewConfig().PreviewTimestamp); - Assert.Null(fork2.GetLivePreviewConfig().PreviewTimestamp); // Reset - Assert.Equal("2024-11-29T14:00:00.000Z", fork3.GetLivePreviewConfig().PreviewTimestamp); - } - - #endregion - - #region Concurrent Modifications - - [Fact] - public void ConcurrentModifications_IndependentStates() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var numberOfForks = 10; - var forks = new ContentstackClient[numberOfForks]; - - for (int i = 0; i < numberOfForks; i++) - { - forks[i] = parentClient.Fork(); - } - - // Act - Concurrent modifications - var tasks = new Task[numberOfForks]; - for (int i = 0; i < numberOfForks; i++) - { - int index = i; // Capture for closure - tasks[i] = Task.Run(() => - { - var config = forks[index].GetLivePreviewConfig(); - config.PreviewTimestamp = $"2024-11-{(index % 12) + 1:D2}-01T{index:D2}:00:00.000Z"; - config.ReleaseId = $"release_{index}"; - - // Simulate some work - Thread.Sleep(10); - - // Modify again - config.PreviewTimestamp = $"2024-11-{(index % 12) + 1:D2}-01T{index:D2}:30:00.000Z"; - }); - } - - Task.WaitAll(tasks); - - // Assert - Each fork should have its final state - for (int i = 0; i < numberOfForks; i++) - { - var expectedTimestamp = $"2024-11-{(i % 12) + 1:D2}-01T{i:D2}:30:00.000Z"; - var expectedReleaseId = $"release_{i}"; - - Assert.Equal(expectedTimestamp, forks[i].GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal(expectedReleaseId, forks[i].GetLivePreviewConfig().ReleaseId); - } - } - - [Fact] - public void HighVolume_ParallelForkOperations_NoStateCorruption() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var operationsPerThread = 100; - var numberOfThreads = 10; - var allResults = new ConcurrentBag(); - - // Act - High volume parallel operations - var tasks = new Task[numberOfThreads]; - for (int threadId = 0; threadId < numberOfThreads; threadId++) - { - int currentThreadId = threadId; // Capture for closure - tasks[threadId] = Task.Run(() => - { - for (int i = 0; i < operationsPerThread; i++) - { - try - { - var fork = parentClient.Fork(); - var timestamp = $"2024-11-29T{currentThreadId:D2}:{i % 60:D2}:00.000Z"; - var releaseId = $"thread_{currentThreadId}_op_{i}"; - - fork.GetLivePreviewConfig().PreviewTimestamp = timestamp; - fork.GetLivePreviewConfig().ReleaseId = releaseId; - - // Verify state immediately - var actualTimestamp = fork.GetLivePreviewConfig().PreviewTimestamp; - var actualReleaseId = fork.GetLivePreviewConfig().ReleaseId; - - if (actualTimestamp == timestamp && actualReleaseId == releaseId) - { - allResults.Add($"SUCCESS_{currentThreadId}_{i}"); - } - else - { - allResults.Add($"CORRUPTION_{currentThreadId}_{i}_{actualTimestamp}_{actualReleaseId}"); - } - } - catch (Exception ex) - { - allResults.Add($"EXCEPTION_{currentThreadId}_{i}_{ex.Message}"); - } - } - }); - } - - Task.WaitAll(tasks); - - // Assert - No state corruption or exceptions - var results = allResults.ToList(); - var expectedResultCount = numberOfThreads * operationsPerThread; - Assert.Equal(expectedResultCount, results.Count); - - var successResults = results.Where(r => r.StartsWith("SUCCESS")).ToList(); - var corruptionResults = results.Where(r => r.StartsWith("CORRUPTION")).ToList(); - var exceptionResults = results.Where(r => r.StartsWith("EXCEPTION")).ToList(); - - Assert.Equal(expectedResultCount, successResults.Count); - Assert.Empty(corruptionResults); - Assert.Empty(exceptionResults); - } - - #endregion - - #region Cache Operations Isolation - - [Fact] - public void ConcurrentCacheOperations_IsolatedBehavior() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var fork1 = parentClient.Fork(); - var fork2 = parentClient.Fork(); - var fork3 = parentClient.Fork(); - - // Set up different cache scenarios - var response1 = CreateMockPreviewResponse("entry1", "ct1"); - var response2 = CreateMockPreviewResponse("entry2", "ct2"); - var response3 = CreateMockPreviewResponse("entry3", "ct3"); - - // Act - Concurrent cache operations - var tasks = new[] - { - Task.Run(() => - { - var config1 = fork1.GetLivePreviewConfig(); - config1.PreviewTimestamp = "2024-11-29T10:00:00.000Z"; - config1.PreviewResponse = response1; - SetInternalProperty(config1, "PreviewResponseFingerprintPreviewTimestamp", config1.PreviewTimestamp); - SetInternalProperty(config1, "PreviewResponseFingerprintReleaseId", config1.ReleaseId); - SetInternalProperty(config1, "PreviewResponseFingerprintLivePreview", GetInternalProperty(config1, "LivePreview")); - }), - Task.Run(() => - { - var config2 = fork2.GetLivePreviewConfig(); - config2.ReleaseId = "concurrent_release"; - config2.PreviewResponse = response2; - SetInternalProperty(config2, "PreviewResponseFingerprintPreviewTimestamp", config2.PreviewTimestamp); - SetInternalProperty(config2, "PreviewResponseFingerprintReleaseId", config2.ReleaseId); - SetInternalProperty(config2, "PreviewResponseFingerprintLivePreview", GetInternalProperty(config2, "LivePreview")); - }), - Task.Run(() => - { - var config3 = fork3.GetLivePreviewConfig(); - SetInternalProperty(config3, "LivePreview", "concurrent_hash"); - config3.PreviewResponse = response3; - SetInternalProperty(config3, "PreviewResponseFingerprintPreviewTimestamp", config3.PreviewTimestamp); - SetInternalProperty(config3, "PreviewResponseFingerprintReleaseId", config3.ReleaseId); - SetInternalProperty(config3, "PreviewResponseFingerprintLivePreview", GetInternalProperty(config3, "LivePreview")); - }) - }; - - Task.WaitAll(tasks); - - // Assert - Each fork has its own cache state - Assert.Same(response1, fork1.GetLivePreviewConfig().PreviewResponse); - Assert.True(fork1.GetLivePreviewConfig().IsCachedPreviewForCurrentQuery()); - - Assert.Same(response2, fork2.GetLivePreviewConfig().PreviewResponse); - Assert.True(fork2.GetLivePreviewConfig().IsCachedPreviewForCurrentQuery()); - - Assert.Same(response3, fork3.GetLivePreviewConfig().PreviewResponse); - Assert.True(fork3.GetLivePreviewConfig().IsCachedPreviewForCurrentQuery()); - - // Cross-verification - each fork doesn't match others' fingerprints - fork1.GetLivePreviewConfig().ReleaseId = "concurrent_release"; - Assert.False(fork1.GetLivePreviewConfig().IsCachedPreviewForCurrentQuery()); // Different fingerprint - } - - #endregion - - #region Error Isolation - - [Fact] - public void ErrorIsolation_InvalidTimestamp_DoesNotAffectOtherForks() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var validFork = parentClient.Fork(); - var invalidFork = parentClient.Fork(); - - // Set up valid state for validFork - validFork.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - validFork.GetLivePreviewConfig().ReleaseId = "valid_release"; - - // Act - Set invalid timestamp on invalidFork - invalidFork.GetLivePreviewConfig().PreviewTimestamp = "invalid-timestamp-format"; - invalidFork.GetLivePreviewConfig().ReleaseId = "invalid_release"; - - // Assert - validFork is unaffected by invalid state in invalidFork - Assert.Equal("2024-11-29T14:30:00.000Z", validFork.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("valid_release", validFork.GetLivePreviewConfig().ReleaseId); - - Assert.Equal("invalid-timestamp-format", invalidFork.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("invalid_release", invalidFork.GetLivePreviewConfig().ReleaseId); - } - - [Fact] - public void ErrorIsolation_ExceptionInOneFork_DoesNotCorruptOthers() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var stableFork = parentClient.Fork(); - var unstableFork = parentClient.Fork(); - - // Set up stable state - stableFork.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - stableFork.GetLivePreviewConfig().PreviewResponse = CreateMockPreviewResponse(); - - // Act & Assert - Exception in unstable fork doesn't affect stable fork - var stableTask = Task.Run(() => - { - // Stable operations - for (int i = 0; i < 100; i++) - { - stableFork.GetLivePreviewConfig().PreviewTimestamp = $"2024-11-29T14:{i % 60:D2}:00.000Z"; - Assert.NotNull(stableFork.GetLivePreviewConfig().PreviewTimestamp); - } - }); - - var unstableTask = Task.Run(() => - { - // Potentially problematic operations - try - { - for (int i = 0; i < 100; i++) - { - if (i == 50) - { - // Simulate error condition - throw new InvalidOperationException("Simulated error in unstable fork"); - } - unstableFork.GetLivePreviewConfig().ReleaseId = $"unstable_release_{i}"; - } - } - catch (InvalidOperationException) - { - // Expected exception - should not affect stable fork - } - }); - - Task.WaitAll(stableTask, unstableTask); - - // Assert - Stable fork completed successfully - Assert.NotNull(stableFork.GetLivePreviewConfig().PreviewTimestamp); - Assert.True(stableFork.GetLivePreviewConfig().PreviewTimestamp.StartsWith("2024-11-29T14:")); - - // Unstable fork state might be partially set, but stable fork is unaffected - Assert.NotEqual(stableFork.GetLivePreviewConfig().ReleaseId, - unstableFork.GetLivePreviewConfig().ReleaseId); - } - - [Fact] - public void ErrorIsolation_NullReferenceInOneFork_IsolatedFailure() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var workingFork = parentClient.Fork(); - var problematicFork = parentClient.Fork(); - - // Set up working state - workingFork.GetLivePreviewConfig().PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - - // Act - Cause null reference in problematic fork - SetInternalProperty(problematicFork, "LivePreviewConfig", null); - - // Assert - Working fork should continue functioning - Assert.Equal("2024-11-29T14:30:00.000Z", workingFork.GetLivePreviewConfig().PreviewTimestamp); - - // Operations on working fork should continue to work - workingFork.GetLivePreviewConfig().ReleaseId = "still_working"; - Assert.Equal("still_working", workingFork.GetLivePreviewConfig().ReleaseId); - - // Problematic fork operations might throw, but don't affect working fork - var exception = Record.Exception(() => problematicFork.ResetLivePreview()); - // Exception is acceptable for problematic fork, but working fork remains unaffected - - Assert.Equal("2024-11-29T14:30:00.000Z", workingFork.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("still_working", workingFork.GetLivePreviewConfig().ReleaseId); - } - - #endregion - - #region Memory and Resource Isolation - - [Fact] - public void ResourceIsolation_IndependentMemoryFootprint() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var numberOfForks = 100; - var forks = new ContentstackClient[numberOfForks]; - - // Create large response objects to test memory isolation - var largeResponse = JObject.Parse($@"{{ - ""entry"": {{ - ""uid"": ""large_entry"", - ""large_field"": ""{new string('x', 1000)}"", - ""another_large_field"": ""{new string('y', 1000)}"" - }} - }}"); - - // Act - Create forks with independent large objects - for (int i = 0; i < numberOfForks; i++) - { - forks[i] = parentClient.Fork(); - forks[i].GetLivePreviewConfig().PreviewResponse = JObject.Parse(largeResponse.ToString()); - forks[i].GetLivePreviewConfig().PreviewTimestamp = $"2024-11-29T{i % 24:D2}:00:00.000Z"; - } - - // Assert - Each fork should have its own copy - for (int i = 0; i < numberOfForks; i++) - { - Assert.NotNull(forks[i].GetLivePreviewConfig().PreviewResponse); - Assert.Equal($"2024-11-29T{i % 24:D2}:00:00.000Z", - forks[i].GetLivePreviewConfig().PreviewTimestamp); - } - - // Modify one fork's response - others should be unaffected - forks[0].GetLivePreviewConfig().PreviewResponse["entry"]["uid"] = "modified_entry"; - - for (int i = 1; i < Math.Min(10, numberOfForks); i++) // Check first 10 for performance - { - Assert.Equal("large_entry", - forks[i].GetLivePreviewConfig().PreviewResponse["entry"]["uid"].ToString()); - } - } - - #endregion - } - - /// - /// Thread-safe collection for concurrent testing - /// - public class ConcurrentBag : List - { - private readonly object _lock = new object(); - - public new void Add(T item) - { - lock (_lock) - { - base.Add(item); - } - } - - public new List ToList() - { - lock (_lock) - { - return new List(this); - } - } - } -} \ No newline at end of file diff --git a/Contentstack.Core.Unit.Tests/TimelinePreviewErrorHandlingTests.cs b/Contentstack.Core.Unit.Tests/TimelinePreviewErrorHandlingTests.cs deleted file mode 100644 index 5d08b65b..00000000 --- a/Contentstack.Core.Unit.Tests/TimelinePreviewErrorHandlingTests.cs +++ /dev/null @@ -1,604 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using AutoFixture; -using Contentstack.Core.Configuration; -using Contentstack.Core.Models; -using Contentstack.Core.Unit.Tests.Helpers; -using Contentstack.Core.Unit.Tests.Mokes; -using Newtonsoft.Json.Linq; -using Xunit; - -namespace Contentstack.Core.Unit.Tests -{ - /// - /// Comprehensive error handling tests for Timeline Preview functionality - /// Tests exception scenarios, error isolation, and graceful degradation - /// - [Trait("Category", "TimelinePreview")] - [Trait("Category", "ErrorHandling")] - [Trait("Category", "Exceptions")] - public class TimelinePreviewErrorHandlingTests : ContentstackClientTestBase - { - #region Invalid Configuration Errors - - [Fact] - public void Fork_NullConfiguration_HandlesGracefully() - { - // Arrange - var client = CreateClient(); - SetInternalProperty(client, "LivePreviewConfig", null); - - // Act & Assert - Should not throw - var exception = Record.Exception(() => - { - var fork = client.Fork(); - Assert.NotNull(fork); - }); - - Assert.Null(exception); - } - - [Fact] - public void ResetLivePreview_CorruptedConfiguration_HandlesGracefully() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Corrupt the configuration - config.PreviewTimestamp = "invalid-timestamp-format!@#$%"; - config.ReleaseId = null; - config.PreviewResponse = JObject.Parse("{}"); // Empty/invalid response - - // Act & Assert - Should not throw - var exception = Record.Exception(() => client.ResetLivePreview()); - Assert.Null(exception); - - // Assert - Configuration should be cleared despite corruption - Assert.Null(config.PreviewTimestamp); - Assert.Null(config.PreviewResponse); - } - - [Fact] - public void IsCachedPreviewForCurrentQuery_CorruptedFingerprints_HandlesGracefully() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .WithPreviewResponse() - .Build(); - - // Set corrupted fingerprint data - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", "corrupted_timestamp"); - SetInternalProperty(config, "PreviewResponseFingerprintReleaseId", "wrong_release"); - SetInternalProperty(config, "PreviewResponseFingerprintLivePreview", "invalid_hash"); - - // Act & Assert - Should handle gracefully and return false - var exception = Record.Exception(() => - { - var result = config.IsCachedPreviewForCurrentQuery(); - Assert.False(result); // Corrupted data should result in cache miss - }); - - Assert.Null(exception); - } - - #endregion - - #region Network and Timeout Errors - - [Fact] - public async Task LivePreviewQueryAsync_NetworkTimeout_HandlesGracefully() - { - // Arrange - var client = CreateClientWithLivePreview(); - var mockHandler = new TimelineMockHttpHandler().ThrowTimeout(); - client.Plugins.Add(mockHandler); - - var query = CreateLivePreviewQuery( - previewTimestamp: "2024-11-29T14:30:00.000Z", - releaseId: "timeout_test_release" - ); - - // Act - method should complete without throwing exception - await client.LivePreviewQueryAsync(query); - - // Assert - timeout is handled gracefully, preview response is not set - var config = client.GetLivePreviewConfig(); - Assert.Null(config.PreviewResponse); // Prefetch failed, no preview response set - Assert.Equal("2024-11-29T14:30:00.000Z", config.PreviewTimestamp); // Basic config still set - Assert.Equal("timeout_test_release", config.ReleaseId); - } - - [Fact] - public async Task LivePreviewQueryAsync_WebException_HandlesGracefully() - { - // Arrange - var client = CreateClientWithLivePreview(); - var mockHandler = new TimelineMockHttpHandler() - .ThrowWebException("Network connection failed"); - client.Plugins.Add(mockHandler); - - var query = CreateLivePreviewQuery( - previewTimestamp: "2024-11-29T14:30:00.000Z", - releaseId: "network_error_release" - ); - - // Act - method should complete without throwing exception - await client.LivePreviewQueryAsync(query); - - // Assert - network error is handled gracefully, preview response is not set - var config = client.GetLivePreviewConfig(); - Assert.Null(config.PreviewResponse); // Prefetch failed, no preview response set - Assert.Equal("2024-11-29T14:30:00.000Z", config.PreviewTimestamp); // Basic config still set - Assert.Equal("network_error_release", config.ReleaseId); - } - - [Fact] - public async Task EntryFetch_NetworkError_FallsBackGracefully() - { - // Arrange - var client = CreateClientWithLivePreview(); - var config = client.GetLivePreviewConfig(); - - // Set up cache miss scenario (force network request) - config.PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - SetInternalProperty(config, "ContentTypeUID", "error_ct"); - SetInternalProperty(config, "EntryUID", "error_entry"); - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", "different_timestamp"); - - var mockHandler = new TimelineMockHttpHandler() - .ThrowWebException("Entry fetch failed"); - client.Plugins.Add(mockHandler); - - var contentType = client.ContentType("error_ct"); - var entry = contentType.Entry("error_entry"); - - // Act & Assert - var exception = await Record.ExceptionAsync(async () => - { - await entry.Fetch(); - }); - - // Network error should propagate but not corrupt client state - Assert.NotNull(exception); - - // Client should remain in consistent state - Assert.Equal("2024-11-29T14:30:00.000Z", config.PreviewTimestamp); - } - - #endregion - - #region Concurrency and Threading Errors - - [Fact] - public async Task ConcurrentOperations_ExceptionInOneThread_DoesNotCorruptOthers() - { - // Arrange - var parentClient = CreateClientWithLivePreview(); - var numberOfThreads = 5; - var operationsPerThread = 10; - var exceptionThreadId = 2; // Thread that will throw exceptions - - var results = new ConcurrentBag(); - - // Act - Concurrent operations with exceptions in one thread - var tasks = new Task[numberOfThreads]; - for (int threadId = 0; threadId < numberOfThreads; threadId++) - { - int currentThreadId = threadId; - tasks[threadId] = Task.Run(async () => - { - try - { - for (int i = 0; i < operationsPerThread; i++) - { - var fork = parentClient.Fork(); - - if (currentThreadId == exceptionThreadId && i >= 5) - { - // Simulate error conditions - throw new InvalidOperationException($"Simulated error in thread {currentThreadId}"); - } - - fork.GetLivePreviewConfig().PreviewTimestamp = $"2024-11-29T{currentThreadId:D2}:{i:D2}:00.000Z"; - fork.GetLivePreviewConfig().ReleaseId = $"thread_{currentThreadId}_op_{i}"; - - // Perform timeline operations - var query = CreateLivePreviewQuery( - previewTimestamp: fork.GetLivePreviewConfig().PreviewTimestamp, - releaseId: fork.GetLivePreviewConfig().ReleaseId - ); - - await fork.LivePreviewQueryAsync(query); - - results.Add(new OperationResult - { - ThreadId = currentThreadId, - OperationId = i, - Success = true, - Timestamp = fork.GetLivePreviewConfig().PreviewTimestamp - }); - } - } - catch (InvalidOperationException ex) - { - results.Add(new OperationResult - { - ThreadId = currentThreadId, - OperationId = -1, - Success = false, - Error = ex.Message - }); - } - }); - } - - await Task.WhenAll(tasks); - - // Assert - var allResults = results.ToList(); - var successfulResults = allResults.Where(r => r.Success).ToList(); - var failedResults = allResults.Where(r => !r.Success).ToList(); - - // Successful threads should complete all operations - var successfulThreads = successfulResults.GroupBy(r => r.ThreadId) - .Where(g => g.Key != exceptionThreadId).ToList(); - - foreach (var threadGroup in successfulThreads) - { - Assert.Equal(operationsPerThread, threadGroup.Count()); - } - - // Exception thread should have some failed operations - Assert.True(failedResults.Any(r => r.ThreadId == exceptionThreadId)); - - // Parent client should remain unaffected - Assert.Null(parentClient.GetLivePreviewConfig().PreviewTimestamp); - Assert.Null(parentClient.GetLivePreviewConfig().ReleaseId); - } - - [Fact] - public void ParallelForkCreation_ExceptionDuringFork_DoesNotCorruptParent() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var numberOfForks = 100; - var corruptionIndex = 50; // Fork that will be corrupted - - var results = new ConcurrentBag(); - - // Act - Parallel fork creation with corruption - Parallel.For(0, numberOfForks, i => - { - try - { - var fork = parentClient.Fork(); - - if (i == corruptionIndex) - { - // Simulate corruption of one fork - SetInternalProperty(fork, "LivePreviewConfig", null); - throw new InvalidOperationException("Fork corruption simulation"); - } - - fork.GetLivePreviewConfig().PreviewTimestamp = $"2024-11-29T{i % 24:D2}:00:00.000Z"; - - results.Add(new ForkResult - { - ForkIndex = i, - Success = true, - Timestamp = fork.GetLivePreviewConfig().PreviewTimestamp - }); - } - catch (Exception ex) - { - results.Add(new ForkResult - { - ForkIndex = i, - Success = false, - Error = ex.Message - }); - } - }); - - // Assert - var allResults = results.ToList(); - var successfulForks = allResults.Where(r => r.Success).ToList(); - var failedForks = allResults.Where(r => !r.Success).ToList(); - - // Should have exactly one failure (the corrupted fork) - Assert.Single(failedForks); - Assert.Equal(corruptionIndex, failedForks[0].ForkIndex); - - // All other forks should succeed - Assert.Equal(numberOfForks - 1, successfulForks.Count); - - // Parent client should remain unaffected - Assert.Equal("2024-11-29T14:30:00.000Z", parentClient.GetLivePreviewConfig().PreviewTimestamp); - Assert.Equal("test_release_123", parentClient.GetLivePreviewConfig().ReleaseId); - } - - #endregion - - #region Malformed Data Errors - - [Fact] - public void IsCachedPreviewForCurrentQuery_MalformedTimestamp_HandlesGracefully() - { - // Arrange - var malformedTimestamps = new[] - { - "not-a-timestamp", - "2024-13-50T25:99:99.999Z", // Invalid date/time - "2024/11/29 14:30:00", // Wrong format - "2024-11-29", // Incomplete - "", // Empty string - " ", // Whitespace only - "null", // String "null" - "undefined" // String "undefined" - }; - - foreach (var malformedTimestamp in malformedTimestamps) - { - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp(malformedTimestamp) - .WithPreviewResponse() - .Build(); - - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", malformedTimestamp); - - // Act & Assert - Should not throw - var exception = Record.Exception(() => - { - var result = config.IsCachedPreviewForCurrentQuery(); - // Result can be true or false, but shouldn't throw - }); - - Assert.Null(exception); - } - } - - [Fact] - public void Timeline_MalformedReleaseId_HandlesGracefully() - { - // Arrange - var malformedReleaseIds = new[] - { - "release\nwith\nnewlines", - "release\twith\ttabs", - "release with spaces and special chars !@#$%^&*()", - new string('x', 10000), // Very long string - "\0\0\0", // Null characters - "🚀🎉💯", // Emojis - "" // Potential XSS - }; - - foreach (var malformedReleaseId in malformedReleaseIds) - { - var client = CreateClientWithTimeline(); - - // Act & Assert - Should not throw - var exception = Record.Exception(() => - { - client.GetLivePreviewConfig().ReleaseId = malformedReleaseId; - client.ResetLivePreview(); - }); - - Assert.Null(exception); - } - } - - [Fact] - public async Task LivePreviewQuery_MalformedQueryParameters_HandlesGracefully() - { - // Arrange - var client = CreateClientWithLivePreview(); - var mockHandler = new TimelineMockHttpHandler() - .ForSuccessfulLivePreview(); - client.Plugins.Add(mockHandler); - - var malformedQueries = new[] - { - // Null values - new Dictionary { ["content_type_uid"] = null, ["entry_uid"] = "test" }, - - // Empty values - new Dictionary { ["content_type_uid"] = "", ["entry_uid"] = "" }, - - // Special characters - new Dictionary - { - ["content_type_uid"] = "ct\nwith\nnewlines", - ["entry_uid"] = "entry\twith\ttabs", - ["preview_timestamp"] = "2024-11-29T14:30:00.000Z\0null" - }, - - // Very long values - new Dictionary - { - ["content_type_uid"] = new string('x', 5000), - ["entry_uid"] = new string('y', 5000) - } - }; - - foreach (var malformedQuery in malformedQueries) - { - // Act & Assert - Should handle gracefully - var exception = await Record.ExceptionAsync(async () => - { - await client.LivePreviewQueryAsync(malformedQuery); - }); - - // May throw validation errors, but should not crash or corrupt state - if (exception != null) - { - Assert.IsNotType(exception); - Assert.IsNotType(exception); - } - } - } - - #endregion - - #region Memory and Resource Errors - - [Fact] - public void Timeline_LargeResponseObjects_HandlesMemoryPressure() - { - // Arrange - var config = TimelineTestDataBuilder.New() - .WithPreviewTimestamp("2024-11-29T14:30:00.000Z") - .Build(); - - // Create extremely large response to test memory handling - var largeArray = new JArray(); - for (int i = 0; i < 10000; i++) - { - largeArray.Add(JObject.Parse($@"{{ - ""id"": {i}, - ""data"": ""{new string('x', 100)}"" - }}")); - } - - var largeResponse = new JObject - { - ["entry"] = new JObject - { - ["uid"] = "large_entry", - ["massive_array"] = largeArray, - ["large_string"] = new string('y', 50000) - } - }; - - // Act & Assert - Should handle large objects without crashing - var exception = Record.Exception(() => - { - config.PreviewResponse = largeResponse; - SetInternalProperty(config, "PreviewResponseFingerprintPreviewTimestamp", config.PreviewTimestamp); - - // Test cache operations with large response - var isCached = config.IsCachedPreviewForCurrentQuery(); - Assert.True(isCached); - }); - - Assert.Null(exception); - } - - [Fact] - public void Fork_MemoryPressure_HandlesGracefully() - { - // Arrange - var parentClient = CreateClientWithTimeline(); - var numberOfForks = 1000; - - // Act & Assert - Should handle many forks without memory issues - var exception = Record.Exception(() => - { - var forks = new ContentstackClient[numberOfForks]; - - for (int i = 0; i < numberOfForks; i++) - { - forks[i] = parentClient.Fork(); - forks[i].GetLivePreviewConfig().PreviewTimestamp = $"2024-11-{(i % 12) + 1:D2}-01T00:00:00.000Z"; - - // Occasionally force garbage collection to test memory pressure - if (i % 100 == 0) - { - GC.Collect(); - GC.WaitForPendingFinalizers(); - } - } - - // Verify forks are still functional - for (int i = 0; i < Math.Min(10, numberOfForks); i++) - { - Assert.NotNull(forks[i].GetLivePreviewConfig()); - } - }); - - Assert.Null(exception); - } - - #endregion - - #region Cleanup and Disposal Errors - - [Fact] - public void ResetLivePreview_AfterObjectDisposal_HandlesGracefully() - { - // Arrange - var client = CreateClientWithTimeline(); - var config = client.GetLivePreviewConfig(); - - // Set up timeline state - config.PreviewResponse = CreateMockPreviewResponse(); - config.PreviewTimestamp = "2024-11-29T14:30:00.000Z"; - - // Simulate object disposal/corruption - config.PreviewResponse = null; - SetInternalProperty(config, "ContentTypeUID", null); - SetInternalProperty(config, "EntryUID", null); - - // Act & Assert - Should handle cleanup gracefully - var exception = Record.Exception(() => - { - client.ResetLivePreview(); - - // Multiple resets should be safe - client.ResetLivePreview(); - client.ResetLivePreview(); - }); - - Assert.Null(exception); - } - - #endregion - - #region Helper Classes - - public class OperationResult - { - public int ThreadId { get; set; } - public int OperationId { get; set; } - public bool Success { get; set; } - public string Timestamp { get; set; } - public string Error { get; set; } - } - - public class ForkResult - { - public int ForkIndex { get; set; } - public bool Success { get; set; } - public string Timestamp { get; set; } - public string Error { get; set; } - } - - public class ConcurrentBag : List - { - private readonly object _lock = new object(); - - public new void Add(T item) - { - lock (_lock) - { - base.Add(item); - } - } - - public new List ToList() - { - lock (_lock) - { - return new List(this); - } - } - } - - #endregion - } -} \ No newline at end of file diff --git a/Contentstack.Core.Unit.Tests/WebRequestAsyncExtensionsUnitTests.cs b/Contentstack.Core.Unit.Tests/WebRequestAsyncExtensionsUnitTests.cs index 4fedb9f6..737355b8 100644 --- a/Contentstack.Core.Unit.Tests/WebRequestAsyncExtensionsUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/WebRequestAsyncExtensionsUnitTests.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Net; +using System.Reflection; using System.Threading.Tasks; using Contentstack.Core.Internals; using Xunit; @@ -9,11 +10,20 @@ namespace Contentstack.Core.Unit.Tests { public class WebRequestAsyncExtensionsUnitTests { + /// + /// Avoids SYSLIB0014 on the test assembly while still constructing the concrete request type the production extensions target. + /// + private static WebRequest CreateHttpWebRequest(string url) + { + var createHttp = typeof(WebRequest).GetMethod("CreateHttp", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(string) }, null); + return (WebRequest)createHttp!.Invoke(null, new object[] { url })!; + } + [Fact] public void GetRequestStreamAsync_WithHttpWebRequest_ReturnsTask() { // Arrange - var request = (HttpWebRequest)WebRequest.Create("http://example.com"); + var request = CreateHttpWebRequest("http://example.com"); request.Method = "POST"; // Act @@ -28,7 +38,7 @@ public void GetRequestStreamAsync_WithHttpWebRequest_ReturnsTask() public void GetResponseAsync_WithHttpWebRequest_ReturnsTask() { // Arrange - var request = (HttpWebRequest)WebRequest.Create("http://example.com"); + var request = CreateHttpWebRequest("http://example.com"); // Act var task = request.GetResponseAsync(); @@ -42,7 +52,7 @@ public void GetResponseAsync_WithHttpWebRequest_ReturnsTask() public async Task GetRequestStreamAsync_ExecutesAsyncOperation() { // Arrange - var request = (HttpWebRequest)WebRequest.Create("http://example.com"); + var request = CreateHttpWebRequest("http://example.com"); request.Method = "POST"; request.Timeout = 1000; // Short timeout to fail fast @@ -53,7 +63,7 @@ public async Task GetRequestStreamAsync_ExecutesAsyncOperation() // Attempt to await - this will execute the async code path // We expect it to fail (network error), but that's ok - we just want coverage // Note: Record.ExceptionAsync can return null if exception is swallowed or handled - var exception = await Record.ExceptionAsync(async () => await task); + _ = await Record.ExceptionAsync(async () => await task); // The task may fail or succeed, but we've executed the code path for coverage Assert.NotNull(task); // Task should be created } @@ -62,7 +72,7 @@ public async Task GetRequestStreamAsync_ExecutesAsyncOperation() public async Task GetResponseAsync_ExecutesAsyncOperation() { // Arrange - var request = (HttpWebRequest)WebRequest.Create("http://example.com"); + var request = CreateHttpWebRequest("http://example.com"); request.Timeout = 1000; // Short timeout to fail fast // Act - The extension method should execute @@ -72,7 +82,7 @@ public async Task GetResponseAsync_ExecutesAsyncOperation() // Attempt to await - this will execute the async code path // We expect it to fail (network error), but that's ok - we just want coverage // Note: Record.ExceptionAsync can return null if exception is swallowed or handled - var exception = await Record.ExceptionAsync(async () => await task); + _ = await Record.ExceptionAsync(async () => await task); // The task may fail or succeed, but we've executed the code path for coverage Assert.NotNull(task); // Task should be created } @@ -81,7 +91,7 @@ public async Task GetResponseAsync_ExecutesAsyncOperation() public void GetRequestStreamAsync_WithWebRequest_ReturnsTask() { // Arrange - var request = WebRequest.Create("http://example.com"); + var request = CreateHttpWebRequest("http://example.com"); request.Method = "POST"; // Act @@ -96,7 +106,7 @@ public void GetRequestStreamAsync_WithWebRequest_ReturnsTask() public void GetResponseAsync_WithWebRequest_ReturnsTask() { // Arrange - var request = WebRequest.Create("http://example.com"); + var request = CreateHttpWebRequest("http://example.com"); // Act var task = request.GetResponseAsync(); diff --git a/Contentstack.Core/Attributes/CSJsonConverterAttribute.cs b/Contentstack.Core/Attributes/CSJsonConverterAttribute.cs index 4020c7ec..d361734e 100644 --- a/Contentstack.Core/Attributes/CSJsonConverterAttribute.cs +++ b/Contentstack.Core/Attributes/CSJsonConverterAttribute.cs @@ -69,9 +69,8 @@ internal static IEnumerable GetCustomAttribute(Type attribute) } } } - catch (Exception ex) + catch { - } } _types[attribute] = result; diff --git a/Contentstack.Core/Configuration/Config.cs b/Contentstack.Core/Configuration/Config.cs index d5ce4794..74b8326c 100644 --- a/Contentstack.Core/Configuration/Config.cs +++ b/Contentstack.Core/Configuration/Config.cs @@ -1,6 +1,5 @@ -using System; -using System.Linq; -using System.Net; +using System.Net; +using Contentstack.Core.Endpoints; using Contentstack.Core.Internals; namespace Contentstack.Core.Configuration @@ -35,8 +34,8 @@ public string Protocol { set { this._Protocol = value; } } - public string Host { - get { return _Host ?? HostURL; } + public string Host { + get { return _Host ?? string.Empty; } set { this._Host = value; } } @@ -76,12 +75,17 @@ public string BaseUrl { get { - string BaseURL = string.Format("{0}://{1}{2}/{3}", - this.Protocol.Trim('/').Trim('\\'), - regionCode(), - this.Host.Trim('/').Trim('\\'), - this.Version.Trim('/').Trim('\\')); - return BaseURL; + string protocol = this.Protocol.Trim('/').Trim('\\'); + string version = this.Version.Trim('/').Trim('\\'); + + // Custom host explicitly set — bypass CDN registry and use it directly. + if (!string.IsNullOrEmpty(_Host)) + return string.Format("{0}://{1}/{2}", protocol, _Host.Trim('/').Trim('\\'), version); + + // Resolve host from CDN-backed regions registry. + string regionId = ContentstackRegionMap.RegionIdMap[Region]; + string host = Endpoint.GetContentstackEndpoint(regionId, "contentDelivery", omitHttps: true); + return string.Format("{0}://{1}/{2}", protocol, host, version); } } @@ -109,22 +113,6 @@ internal string getBaseUrl (LivePreviewConfig livePreviewConfig, string contentT return BaseUrl; } - internal string regionCode() - { - if (Region == ContentstackRegion.US) return ""; - ContentstackRegionCode[] regionCodes = Enum.GetValues(typeof(ContentstackRegionCode)).Cast().ToArray(); - return string.Format("{0}-", regionCodes[(int)Region].ToString().Replace("_", "-")); - } - - internal string HostURL - { - get - { - if (Region == ContentstackRegion.EU || Region == ContentstackRegion.AZURE_EU || Region == ContentstackRegion.AZURE_NA || Region == ContentstackRegion.GCP_NA || Region==ContentstackRegion.AU) - return "cdn.contentstack.com"; - return "cdn.contentstack.io"; - } - } #endregion } } diff --git a/Contentstack.Core/Configuration/ContentstackOptions.cs b/Contentstack.Core/Configuration/ContentstackOptions.cs index 62c1cf59..9c85a25f 100644 --- a/Contentstack.Core/Configuration/ContentstackOptions.cs +++ b/Contentstack.Core/Configuration/ContentstackOptions.cs @@ -75,6 +75,27 @@ public ContentstackOptions() Timeout = 30000; // Set default value } + /// + /// Resolves the token for the access_token HTTP header. Obsolete AccessToken is still honored when set (before DeliveryToken). + /// + internal bool TryGetAccessTokenHeaderValue(out string token) + { +#pragma warning disable CS0618 // AccessToken is obsolete; retained for backward compatibility until removed from the API surface + if (AccessToken != null) + { + token = AccessToken; + return true; + } +#pragma warning restore CS0618 + if (DeliveryToken != null) + { + token = DeliveryToken; + return true; + } + token = null; + return false; + } + /// /// TheEarlyAccessHeader used to set service which the user has early access to. /// diff --git a/Contentstack.Core/Configuration/LivePreviewConfig.cs b/Contentstack.Core/Configuration/LivePreviewConfig.cs index 07c5310d..92d6c827 100644 --- a/Contentstack.Core/Configuration/LivePreviewConfig.cs +++ b/Contentstack.Core/Configuration/LivePreviewConfig.cs @@ -1,5 +1,4 @@ -using System; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; namespace Contentstack.Core.Configuration { @@ -12,25 +11,8 @@ public class LivePreviewConfig internal string LivePreview { get; set; } internal string ContentTypeUID { get; set; } internal string EntryUID { get; set; } - internal JObject PreviewResponse { get; set; } - - /// - /// Snapshot of preview_timestamp / release_id / live_preview when was set (prefetch). - /// Prevents Entry.Fetch from short-circuiting with a draft from a previous Live Preview query. - /// - internal string PreviewResponseFingerprintPreviewTimestamp { get; set; } - internal string PreviewResponseFingerprintReleaseId { get; set; } - internal string PreviewResponseFingerprintLivePreview { get; set; } - + internal JsonObject PreviewResponse { get; set; } public string ReleaseId {get; set;} public string PreviewTimestamp {get; set;} - - internal bool IsCachedPreviewForCurrentQuery() - { - if (PreviewResponse == null) return false; - return string.Equals(PreviewTimestamp, PreviewResponseFingerprintPreviewTimestamp, StringComparison.Ordinal) - && string.Equals(ReleaseId, PreviewResponseFingerprintReleaseId, StringComparison.Ordinal) - && string.Equals(LivePreview, PreviewResponseFingerprintLivePreview, StringComparison.Ordinal); - } } } diff --git a/Contentstack.Core/Contentstack.Core.csproj b/Contentstack.Core/Contentstack.Core.csproj index d919ec62..c602db54 100644 --- a/Contentstack.Core/Contentstack.Core.csproj +++ b/Contentstack.Core/Contentstack.Core.csproj @@ -1,7 +1,8 @@  - netstandard2.0;net47;net472; + net10.0 + latest contentstack.csharp Contentstack .NET SDK for the Contentstack Content Delivery API. @@ -28,10 +29,11 @@ - + - + + @@ -46,11 +48,28 @@ + + + + + true + contentFiles/cs/any/Scripts/refresh-region.py + Content + false + + + true + build/contentstack.csharp.targets + + diff --git a/Contentstack.Core/ContentstackClient.cs b/Contentstack.Core/ContentstackClient.cs index 0c04d52d..3e2b3901 100644 --- a/Contentstack.Core/ContentstackClient.cs +++ b/Contentstack.Core/ContentstackClient.cs @@ -1,17 +1,18 @@ -using System; +using System; using System.Collections.Generic; using Contentstack.Core.Internals; using Contentstack.Core.Configuration; using Microsoft.Extensions.Options; using Contentstack.Core.Models; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json; using System.Linq; using System.Threading.Tasks; using System.Net; using System.IO; using System.Collections; using Contentstack.Core.Interfaces; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using Contentstack.Utils.Converters; namespace Contentstack.Core @@ -22,9 +23,9 @@ namespace Contentstack.Core public class ContentstackClient { /// - /// Gets or sets the settings that should be used for deserialization. + /// Gets or sets the options used for JSON serialization and deserialization (System.Text.Json). /// - public JsonSerializerSettings SerializerSettings { get; set; } = new JsonSerializerSettings(); + public JsonSerializerOptions SerializerOptions { get; set; } = new JsonSerializerOptions(); #region Internal Variables @@ -35,7 +36,6 @@ internal string StackApiKey } private ContentstackOptions _options; - internal JsonSerializer Serializer => JsonSerializer.Create(SerializerSettings); internal string _SyncUrl { get @@ -62,100 +62,6 @@ private string _Url private string currentContenttypeUid = null; private string currentEntryUid = null; public List Plugins { get; set; } = new List(); - - private static LivePreviewConfig CloneLivePreviewConfig(LivePreviewConfig source) - { - if (source == null) return null; - - return new LivePreviewConfig - { - Enable = source.Enable, - Host = source.Host, - ManagementToken = source.ManagementToken, - PreviewToken = source.PreviewToken, - ReleaseId = source.ReleaseId, - PreviewTimestamp = source.PreviewTimestamp, - - // internal state (same assembly) - LivePreview = source.LivePreview, - ContentTypeUID = source.ContentTypeUID, - EntryUID = source.EntryUID, - PreviewResponse = source.PreviewResponse, - PreviewResponseFingerprintPreviewTimestamp = source.PreviewResponseFingerprintPreviewTimestamp, - PreviewResponseFingerprintReleaseId = source.PreviewResponseFingerprintReleaseId, - PreviewResponseFingerprintLivePreview = source.PreviewResponseFingerprintLivePreview - }; - } - - private static ContentstackOptions CloneOptions(ContentstackOptions source) - { - if (source == null) return null; - - return new ContentstackOptions - { - ApiKey = source.ApiKey, - AccessToken = source.AccessToken, - DeliveryToken = source.DeliveryToken, - Environment = source.Environment, - Host = source.Host, - Proxy = source.Proxy, - Region = source.Region, - Version = source.Version, - Branch = source.Branch, - Timeout = source.Timeout, - EarlyAccessHeader = source.EarlyAccessHeader, - LivePreview = CloneLivePreviewConfig(source.LivePreview) - }; - } - - /// - /// Clears any in-memory Live Preview context (hash, release, timestamp, content type, entry). - /// Useful when switching back to the Delivery API after using Live Preview / Timeline preview. - /// - public void ResetLivePreview() - { - if (this.LivePreviewConfig == null) return; - - this.LivePreviewConfig.LivePreview = null; - this.LivePreviewConfig.ReleaseId = null; - this.LivePreviewConfig.PreviewTimestamp = null; - this.LivePreviewConfig.ContentTypeUID = null; - this.LivePreviewConfig.EntryUID = null; - this.LivePreviewConfig.PreviewResponse = null; - this.LivePreviewConfig.PreviewResponseFingerprintPreviewTimestamp = null; - this.LivePreviewConfig.PreviewResponseFingerprintReleaseId = null; - this.LivePreviewConfig.PreviewResponseFingerprintLivePreview = null; - } - - /// - /// Creates a new client instance with the same configuration but isolated in-memory state. - /// Use this to safely perform Timeline comparisons (left/right) without shared Live Preview context. - /// - public ContentstackClient Fork() - { - var forked = new ContentstackClient(CloneOptions(_options)); - - // Clone current LivePreviewConfig state (not from original options) - if (this.LivePreviewConfig != null) - { - forked.LivePreviewConfig = CloneLivePreviewConfig(this.LivePreviewConfig); - } - - // Preserve any runtime header mutations (e.g., custom headers added via SetHeader). - if (this._LocalHeaders != null) - { - foreach (var kvp in this._LocalHeaders) - { - forked.SetHeader(kvp.Key, kvp.Value?.ToString()); - } - } - - // Carry over current content type / entry hints (used when live preview query omits them) - forked.currentContenttypeUid = this.currentContenttypeUid; - forked.currentEntryUid = this.currentEntryUid; - - return forked; - } /// /// Initializes a instance of the class. /// @@ -178,13 +84,9 @@ public ContentstackClient(IOptions options) this.StackApiKey = _options.ApiKey; this._LocalHeaders = new Dictionary(); this.SetHeader("api_key", _options.ApiKey); - if (_options.AccessToken != null) - { - this.SetHeader("access_token", _options.AccessToken); - } - else if (_options.DeliveryToken != null) + if (_options.TryGetAccessTokenHeaderValue(out var authToken)) { - this.SetHeader("access_token", _options.DeliveryToken); + this.SetHeader("access_token", authToken); } if (_options.EarlyAccessHeader != null) { @@ -210,7 +112,7 @@ public ContentstackClient(IOptions options) this.SetConfig(cnfig); if (_options.LivePreview != null) { - this.LivePreviewConfig = CloneLivePreviewConfig(_options.LivePreview); + this.LivePreviewConfig = _options.LivePreview; } else { @@ -234,18 +136,22 @@ public ContentstackClient(IOptions options) throw new InvalidOperationException(ErrorMessages.LivePreviewTokenMissing); } } - this.SerializerSettings.DateParseHandling = DateParseHandling.None; - this.SerializerSettings.DateFormatHandling = DateFormatHandling.IsoDateFormat; - this.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; - this.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + this.SerializerOptions.PropertyNameCaseInsensitive = true; + this.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + this.SerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip; + this.SerializerOptions.AllowTrailingCommas = true; + this.SerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; foreach (Type t in CSJsonConverterAttribute.GetCustomAttribute(typeof(CSJsonConverterAttribute))) { - SerializerSettings.Converters.Add((JsonConverter)Activator.CreateInstance(t)); + if (Activator.CreateInstance(t) is JsonConverter converter) + { + SerializerOptions.Converters.Add(converter); + } } // Handles deserialization of embedded entries and assets in _embedded_items responses. - SerializerSettings.Converters.Add(new EmbeddedObjectConverter()); + SerializerOptions.Converters.Add(new EmbeddedObjectConverter()); } public ContentstackClient(ContentstackOptions options) : @@ -313,19 +219,7 @@ internal static ContentstackException GetContentstackError(Exception ex) using (var reader = new StreamReader(stream)) { errorMessage = reader.ReadToEnd(); - JObject data = JObject.Parse(errorMessage.Replace("\r\n", "")); - - JToken token = data["error_code"]; - if (token != null) - errorCode = token.Value(); - - token = data["error_message"]; - if (token != null) - errorMessage = token.Value(); - - token = data["errors"]; - if (token != null) - errors = token.ToObject>(); + ApiErrorBodyParser.TryApply(errorMessage.Replace("\r\n", ""), ref errorCode, ref errorMessage, ref errors); var response = exResp as HttpWebResponse; if (response != null) @@ -429,8 +323,16 @@ public async Task GetContentTypes(Dictionary param = null { HttpRequestHandler RequestHandler = new HttpRequestHandler(this); var outputResult = await RequestHandler.ProcessRequest(_Url, headers, mainJson, Branch: this.Config.Branch, timeout: this.Config.Timeout, proxy: this.Config.Proxy); - JObject data = JsonConvert.DeserializeObject(outputResult.Replace("\r\n", ""), this.SerializerSettings); - IList contentTypes = (IList)data["content_types"]; + JsonObject data = JsonNode.Parse(outputResult.Replace("\r\n", ""))!.AsObject(); + var ctArray = data["content_types"]?.AsArray(); + var contentTypes = new List(); + if (ctArray != null) + { + foreach (var item in ctArray) + { + contentTypes.Add(JsonNodeConversion.JsonNodeToClr(item)); + } + } return contentTypes; } catch (Exception ex) @@ -439,12 +341,7 @@ public async Task GetContentTypes(Dictionary param = null } } - /// - /// Fetches draft entry JSON from the Live Preview host (Java Stack.livePreviewQuery equivalent). - /// Always uses the configured preview host so the call succeeds even when the delivery base URL - /// would still point at CDN (e.g. live_preview hash is "init"). - /// - private async Task GetLivePreviewData() + private async Task GetLivePreviewData() { Dictionary headerAll = new Dictionary(); @@ -489,25 +386,11 @@ private async Task GetLivePreviewData() try { HttpRequestHandler RequestHandler = new HttpRequestHandler(this); - string basePreview = this.Config.getLivePreviewUrl(this.LivePreviewConfig); - string URL = String.Format("{0}/content_types/{1}/entries/{2}", basePreview, this.LivePreviewConfig.ContentTypeUID, this.LivePreviewConfig.EntryUID); + //string branch = this.Config.Branch ? this.Config.Branch : "main"; + string URL = String.Format("{0}/content_types/{1}/entries/{2}", this.Config.getBaseUrl(this.LivePreviewConfig, this.LivePreviewConfig.ContentTypeUID), this.LivePreviewConfig.ContentTypeUID, this.LivePreviewConfig.EntryUID); var outputResult = await RequestHandler.ProcessRequest(URL, headerAll, mainJson, Branch: this.Config.Branch, isLivePreview: true, timeout: this.Config.Timeout, proxy: this.Config.Proxy); - JObject data = JsonConvert.DeserializeObject(outputResult.Replace("\r\n", ""), this.SerializerSettings); - if (data == null) return null; - if (data["entry"] is JObject single && single.HasValues) - return single; - if (data["entries"] is JArray arr && arr.Count > 0) - { - string targetUid = this.LivePreviewConfig.EntryUID; - foreach (var token in arr) - { - if (token is JObject jo && jo["uid"] != null - && string.Equals(jo["uid"].ToString(), targetUid, StringComparison.Ordinal)) - return jo; - } - return arr[0] as JObject; - } - return null; + JsonObject data = JsonNode.Parse(outputResult.Replace("\r\n", ""))!.AsObject(); + return data["entry"]!.AsObject(); } catch (Exception ex) { @@ -729,10 +612,6 @@ public async Task LivePreviewQueryAsync(Dictionary query) this.LivePreviewConfig.LivePreview = null; this.LivePreviewConfig.PreviewTimestamp = null; this.LivePreviewConfig.ReleaseId = null; - this.LivePreviewConfig.PreviewResponse = null; - this.LivePreviewConfig.PreviewResponseFingerprintPreviewTimestamp = null; - this.LivePreviewConfig.PreviewResponseFingerprintReleaseId = null; - this.LivePreviewConfig.PreviewResponseFingerprintLivePreview = null; if (query.Keys.Contains("content_type_uid")) { string contentTypeUID = null; @@ -776,28 +655,10 @@ public async Task LivePreviewQueryAsync(Dictionary query) query.TryGetValue("preview_timestamp", out PreviewTimestamp); this.LivePreviewConfig.PreviewTimestamp = PreviewTimestamp; } - - if (this.LivePreviewConfig.Enable - && !string.IsNullOrEmpty(this.LivePreviewConfig.Host) - && !string.IsNullOrEmpty(this.LivePreviewConfig.ContentTypeUID) - && !string.IsNullOrEmpty(this.LivePreviewConfig.EntryUID)) - { - try - { - var draft = await GetLivePreviewData(); - if (draft != null && draft.Type == JTokenType.Object && draft.HasValues) - { - this.LivePreviewConfig.PreviewResponse = draft; - this.LivePreviewConfig.PreviewResponseFingerprintPreviewTimestamp = this.LivePreviewConfig.PreviewTimestamp; - this.LivePreviewConfig.PreviewResponseFingerprintReleaseId = this.LivePreviewConfig.ReleaseId; - this.LivePreviewConfig.PreviewResponseFingerprintLivePreview = this.LivePreviewConfig.LivePreview; - } - } - catch - { - // Prefetch failed: Entry.Fetch still uses preview headers on the network path. - } - } + //if (!string.IsNullOrEmpty(this.LivePreviewConfig.LivePreview)) + //{ + // this.LivePreviewConfig.PreviewResponse = await GetLivePreviewData(); + //} } /// @@ -1006,7 +867,7 @@ private async Task GetResultAsync(string Init = "false", SyncType Syn { HttpRequestHandler requestHandler = new HttpRequestHandler(this); string js = await requestHandler.ProcessRequest(_SyncUrl, _LocalHeaders, mainJson, Branch: this.Config.Branch, timeout: this.Config.Timeout, proxy: this.Config.Proxy); - SyncStack stackSyncOutput = JsonConvert.DeserializeObject(js); + SyncStack stackSyncOutput = JsonSerializer.Deserialize(js, SerializerOptions); return stackSyncOutput; } catch (Exception ex) diff --git a/Contentstack.Core/Endpoints/Endpoint.cs b/Contentstack.Core/Endpoints/Endpoint.cs new file mode 100644 index 00000000..418d8539 --- /dev/null +++ b/Contentstack.Core/Endpoints/Endpoint.cs @@ -0,0 +1,287 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Contentstack.Core.Endpoints +{ + /// + /// Resolves Contentstack service URLs for any supported region. + /// + /// All public methods are static — no instantiation required. + /// + /// Example: + /// + /// // Full URL + /// string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + /// // → "https://cdn.contentstack.io" + /// + /// // Host only (omit https://) — useful for SDK host configuration + /// string host = Endpoint.GetContentstackEndpoint("eu", "contentDelivery", omitHttps: true); + /// // → "eu-cdn.contentstack.com" + /// + /// // All endpoints for a region + /// Dictionary<string, string> all = Endpoint.GetContentstackEndpoint("azure-na"); + /// // → { "contentDelivery": "...", "contentManagement": "...", ... } + /// + /// + public static class Endpoint + { + private const string RegionsUrl = "https://artifacts.contentstack.com/regions.json"; + + // Module-level cache — loaded once per process, shared across all calls. + private static JsonElement[]? _regionsData; + private static readonly object _cacheLock = new object(); + private static readonly HttpClient _httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(30) + }; + + /// + /// Resolves a single Contentstack service endpoint URL for the given region. + /// + /// + /// Region ID or alias (case-insensitive). Examples: "na", "us", "eu", "AWS-NA", "azure_eu", "gcp-na". + /// + /// + /// Service key. Valid keys include: "contentDelivery", "contentManagement", "auth", + /// "graphqlDelivery", "preview", "images", "assets", "automate", "launch", + /// "developerHub", "brandKit", "genAI", "personalizeManagement", + /// "personalizeEdge", "composableStudio", "assetManagement". + /// + /// + /// When true, strips the https:// scheme from the returned URL. + /// Useful when passing the host to an SDK that constructs its own URLs. + /// + /// The endpoint URL string for the specified service. + /// If region is empty or whitespace. + /// If region or service is not found. + /// If the registry cannot be read or is corrupt. + public static string GetContentstackEndpoint(string region, string service, bool omitHttps = false) + { + if (string.IsNullOrWhiteSpace(region)) + throw new ArgumentException("Empty region provided. Please put valid region.", nameof(region)); + + var regions = LoadRegions(); + string normalized = region.Trim().ToLowerInvariant(); + var regionEl = FindRegion(regions, normalized); + + if (regionEl == null) + throw new KeyNotFoundException($"Invalid region: {region}"); + + var endpoints = regionEl.Value.GetProperty("endpoints"); + if (!endpoints.TryGetProperty(service, out var urlEl)) + { + string regionId = regionEl.Value.GetProperty("id").GetString()!; + throw new KeyNotFoundException($"Service \"{service}\" not found for region \"{regionId}\""); + } + + string url = urlEl.GetString()!; + return omitHttps ? StripHttps(url) : url; + } + + /// + /// Returns all service endpoint URLs for the given region. + /// + /// Region ID or alias (case-insensitive). + /// When true, strips the https:// scheme from all returned URLs. + /// Dictionary mapping service keys to endpoint URLs. + /// If region is empty or whitespace. + /// If region is not found. + /// If the registry cannot be read or is corrupt. + public static Dictionary GetContentstackEndpoint(string region, bool omitHttps = false) + { + if (string.IsNullOrWhiteSpace(region)) + throw new ArgumentException("Empty region provided. Please put valid region.", nameof(region)); + + var regions = LoadRegions(); + string normalized = region.Trim().ToLowerInvariant(); + var regionEl = FindRegion(regions, normalized); + + if (regionEl == null) + throw new KeyNotFoundException($"Invalid region: {region}"); + + var result = new Dictionary(); + var endpoints = regionEl.Value.GetProperty("endpoints"); + foreach (var ep in endpoints.EnumerateObject()) + { + string url = ep.Value.GetString()!; + result[ep.Name] = omitHttps ? StripHttps(url) : url; + } + return result; + } + + /// + /// Clears the in-memory region cache. Intended for testing only — forces the + /// next call to re-read regions.json from disk or re-download from CDN. + /// + public static void ResetCache() + { + lock (_cacheLock) + { + _regionsData = null; + } + } + + // ------------------------------------------------------------------ + // Internal helpers + // ------------------------------------------------------------------ + + /// + /// Load and cache the regions registry. + /// + /// Resolution order: + /// 1. In-memory cache — zero I/O after the first call in a process + /// 2. Local file on disk — Assets/regions.json next to the DLL + /// (written by DownloadAndSave or refresh-region.cs) + /// 3. CDN download — fetches from artifacts.contentstack.com, + /// writes to disk for future calls (silent on failure) + /// + private static JsonElement[] LoadRegions() + { + lock (_cacheLock) + { + if (_regionsData != null) + return _regionsData; + + string localFile = GetLocalFilePath(); + + // Step 2 — local file on disk + string? json = ReadLocalFile(localFile); + + // Step 3 — CDN download, writes to disk so next startup skips this step + if (json == null) + json = DownloadAndSave(localFile); + + if (json == null) + throw new InvalidOperationException( + "contentstack_csharp: regions.json not found and could not be downloaded. " + + "Run 'dotnet run Scripts/refresh-region.cs' and ensure network access."); + + JsonDocument doc; + try + { + doc = JsonDocument.Parse(json); + } + catch (JsonException ex) + { + throw new InvalidOperationException( + "contentstack_csharp: regions.json is corrupt. " + + "Run 'dotnet run Scripts/refresh-region.cs' to re-download it.", ex); + } + + if (!doc.RootElement.TryGetProperty("regions", out var regionsEl) || + regionsEl.ValueKind != JsonValueKind.Array) + { + throw new InvalidOperationException( + "contentstack_csharp: regions.json is corrupt. " + + "Run 'dotnet run Scripts/refresh-region.cs' to re-download it."); + } + + var list = new List(); + foreach (var r in regionsEl.EnumerateArray()) + list.Add(r.Clone()); + + _regionsData = list.ToArray(); + return _regionsData; + } + } + + /// + /// Returns the path to regions.json next to the DLL. + /// + internal static string GetLocalFilePath() + { + string assemblyDir = Path.GetDirectoryName(typeof(Endpoint).Assembly.Location) + ?? AppContext.BaseDirectory; + return Path.Combine(assemblyDir, "Assets", "regions.json"); + } + + /// + /// Reads regions.json from disk. Returns null if the file does not exist. + /// + private static string? ReadLocalFile(string path) + { + if (!File.Exists(path)) + return null; + try + { + return File.ReadAllText(path); + } + catch + { + return null; + } + } + + /// + /// Downloads regions.json from the CDN and writes it to disk so that future + /// process startups read from the local file instead of downloading again. + /// Silent on all failures (network error, permission denied). + /// + private static string? DownloadAndSave(string dest) + { + try + { + var task = _httpClient.GetStringAsync(RegionsUrl); + task.Wait(); + string data = task.Result; + + using var doc = JsonDocument.Parse(data); + if (!doc.RootElement.TryGetProperty("regions", out _)) + return null; + + // Write to disk — next startup reads from local file (Step 2). + // Silent on PermissionError or read-only filesystem. + try + { + Directory.CreateDirectory(Path.GetDirectoryName(dest)!); + File.WriteAllText(dest, data); + } + catch { } + + return data; + } + catch + { + return null; + } + } + + /// + /// Two-pass region lookup: ID match wins over alias match. + /// Input must already be lowercased and trimmed. + /// + private static JsonElement? FindRegion(JsonElement[] regions, string normalized) + { + // Pass 1 — exact id match + foreach (var r in regions) + { + if (r.TryGetProperty("id", out var id) && + id.GetString()?.ToLowerInvariant() == normalized) + return r; + } + + // Pass 2 — alias match + foreach (var r in regions) + { + if (!r.TryGetProperty("alias", out var aliases)) continue; + foreach (var alias in aliases.EnumerateArray()) + { + if (alias.GetString()?.ToLowerInvariant() == normalized) + return r; + } + } + + return null; + } + + private static string StripHttps(string url) + { + return Regex.Replace(url, @"^https?://", string.Empty); + } + } +} diff --git a/Contentstack.Core/Internals/ApiErrorBodyParser.cs b/Contentstack.Core/Internals/ApiErrorBodyParser.cs new file mode 100644 index 00000000..6a4e6f05 --- /dev/null +++ b/Contentstack.Core/Internals/ApiErrorBodyParser.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Contentstack.Core.Internals +{ + /// + /// Parses JSON error bodies from the Contentstack API (error_code, error_message, errors). + /// + internal static class ApiErrorBodyParser + { + internal static void TryApply( + string body, + ref int errorCode, + ref string errorMessage, + ref Dictionary errors) + { + if (string.IsNullOrWhiteSpace(body)) + { + return; + } + + try + { + var node = JsonNode.Parse(body.Replace("\r\n", "")); + if (node is not JsonObject data) + { + return; + } + + if (data["error_code"] is JsonValue ec) + { + try + { + errorCode = ec.GetValue(); + } + catch + { + if (int.TryParse(ec.ToString(), out var p)) + { + errorCode = p; + } + } + } + + if (data["error_message"] is JsonValue em) + { + var s = em.GetValue(); + if (s != null) + { + errorMessage = s; + } + } + + if (data["errors"] is JsonObject errObj) + { + errors = JsonNodeConversion.JsonObjectToDictionary(errObj); + } + else if (data["errors"] is JsonArray) + { + errors = new Dictionary + { + { "errors", JsonNodeConversion.JsonNodeToClr(data["errors"]) } + }; + } + } + catch (JsonException) + { + } + } + } +} diff --git a/Contentstack.Core/Internals/AssetJsonConverter.cs b/Contentstack.Core/Internals/AssetJsonConverter.cs index 677682d1..dc009ba2 100644 --- a/Contentstack.Core/Internals/AssetJsonConverter.cs +++ b/Contentstack.Core/Internals/AssetJsonConverter.cs @@ -1,24 +1,29 @@ using System; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using Contentstack.Core.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Contentstack.Core.Internals { [CSJsonConverter("AssetJsonConverter")] public class AssetJsonConverter : JsonConverter { - public override Asset ReadJson(JsonReader reader, Type objectType, Asset existingValue, bool hasExistingValue, JsonSerializer serializer) + public override Asset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - JObject jObject = JObject.Load(reader); - JsonSerializerSettings SerializerSettings = new JsonSerializerSettings(); - JsonSerializer Serializer = JsonSerializer.Create(SerializerSettings); - Asset asset = jObject.ToObject(Serializer); - asset.ParseObject(jObject); + using var doc = JsonDocument.ParseValue(ref reader); + var jsonObject = JsonNode.Parse(doc.RootElement.GetRawText())!.AsObject(); + + var asset = JsonSerializer.Deserialize( + jsonObject.ToJsonString(), + ContentstackJsonDefaults.ModelDeserializeOnly); + + asset ??= new Asset(); + asset.ParseObject(jsonObject); return asset; } - public override void WriteJson(JsonWriter writer, Asset value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, Asset value, JsonSerializerOptions options) { throw AssetException.CreateForJsonConversionError(); } diff --git a/Contentstack.Core/Internals/ContentstackConvert.cs b/Contentstack.Core/Internals/ContentstackConvert.cs index 369bc32b..57f7e2a0 100644 --- a/Contentstack.Core/Internals/ContentstackConvert.cs +++ b/Contentstack.Core/Internals/ContentstackConvert.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using System.Text.RegularExpressions; @@ -19,7 +20,7 @@ public static Int32 ToInt32(object input) { if (e.Source != null) { - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } } @@ -38,7 +39,7 @@ public static bool ToBoolean(object input) { if (e.Source != null) { - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } } @@ -57,7 +58,7 @@ public static string ToString(object input, string defaultValue = "") { if (e.Source != null) { - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } } @@ -76,7 +77,7 @@ public static double ToDouble(object input) { if (e.Source != null) { - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } } @@ -95,7 +96,7 @@ public static decimal ToDecimal(object input) { if (e.Source != null) { - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } } @@ -114,7 +115,7 @@ public static DateTime ToDateTime(object input) { if (e.Source != null) { - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } } @@ -132,7 +133,7 @@ public static string ToISODate(object input) { if (e.Source != null) { - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } } return now.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'sszzz"); @@ -186,7 +187,7 @@ public static object GetValue(string value) { if (e.Source != null) { - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } } diff --git a/Contentstack.Core/Internals/ContentstackJsonDefaults.cs b/Contentstack.Core/Internals/ContentstackJsonDefaults.cs new file mode 100644 index 00000000..2e91391b --- /dev/null +++ b/Contentstack.Core/Internals/ContentstackJsonDefaults.cs @@ -0,0 +1,26 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Contentstack.Core.Internals +{ + /// + /// Options used inside custom converters when deserializing POCO properties without re-entering SDK converters (matches former Newtonsoft empty usage). + /// + internal static class ContentstackJsonDefaults + { + internal static readonly JsonSerializerOptions ModelDeserializeOnly = CreateModelDeserializeOnly(); + + private static JsonSerializerOptions CreateModelDeserializeOnly() + { + var o = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + }; + return o; + } + } +} diff --git a/Contentstack.Core/Internals/ContentstackRegion.cs b/Contentstack.Core/Internals/ContentstackRegion.cs index 2d8f6382..a27c8aa5 100644 --- a/Contentstack.Core/Internals/ContentstackRegion.cs +++ b/Contentstack.Core/Internals/ContentstackRegion.cs @@ -1,4 +1,6 @@ -namespace Contentstack.Core.Internals +using System.Collections.Generic; + +namespace Contentstack.Core.Internals { /// /// Contentstack region. @@ -31,7 +33,12 @@ public enum ContentstackRegion /// /// To specify AWS_AU region. /// - AU + AU, + + /// + /// To specify GCP_EU region. + /// + GCP_EU } @@ -48,6 +55,25 @@ internal enum ContentstackRegionCode gcp_na, au, - + + gcp_eu, + } + + /// + /// Maps enum values to the region ID strings + /// used by the Contentstack regions registry (artifacts.contentstack.com/regions.json). + /// + internal static class ContentstackRegionMap + { + internal static readonly Dictionary RegionIdMap = new Dictionary + { + { ContentstackRegion.US, "na" }, + { ContentstackRegion.EU, "eu" }, + { ContentstackRegion.AZURE_EU, "azure-eu" }, + { ContentstackRegion.AZURE_NA, "azure-na" }, + { ContentstackRegion.GCP_NA, "gcp-na" }, + { ContentstackRegion.AU, "au" }, + { ContentstackRegion.GCP_EU, "gcp-eu" }, + }; } } diff --git a/Contentstack.Core/Internals/EntryJsonConverter.cs b/Contentstack.Core/Internals/EntryJsonConverter.cs index 230e1b86..403f2da0 100644 --- a/Contentstack.Core/Internals/EntryJsonConverter.cs +++ b/Contentstack.Core/Internals/EntryJsonConverter.cs @@ -1,26 +1,33 @@ using System; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using Contentstack.Core.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Contentstack.Core.Internals { [CSJsonConverter("EntryJsonConverter")] public class EntryJsonConverter : JsonConverter { - public override Entry ReadJson(JsonReader reader, Type objectType, Entry existingValue, bool hasExistingValue, JsonSerializer serializer) + public override Entry Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - JObject jObject = JObject.Load(reader); - JsonSerializerSettings SerializerSettings = new JsonSerializerSettings(); - JsonSerializer Serializer = JsonSerializer.Create(SerializerSettings); - Entry entry = jObject.ToObject(Serializer); - entry.ParseObject(jObject); + using var doc = JsonDocument.ParseValue(ref reader); + var jsonObject = JsonNode.Parse(doc.RootElement.GetRawText())!.AsObject(); + + var entry = JsonSerializer.Deserialize( + jsonObject.ToJsonString(), + ContentstackJsonDefaults.ModelDeserializeOnly); + + entry ??= new Entry(); + entry.ParseObject(jsonObject); return entry; } - public override void WriteJson(JsonWriter writer, Entry value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, Entry value, JsonSerializerOptions options) { - + // Serialization of Entry is handled outside this converter during normal flows. + writer.WriteStartObject(); + writer.WriteEndObject(); } } } diff --git a/Contentstack.Core/Internals/HttpRequestHandler.cs b/Contentstack.Core/Internals/HttpRequestHandler.cs index 8d367497..1a20deb9 100644 --- a/Contentstack.Core/Internals/HttpRequestHandler.cs +++ b/Contentstack.Core/Internals/HttpRequestHandler.cs @@ -5,8 +5,7 @@ using System.Net; using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; namespace Contentstack.Core.Internals { @@ -48,7 +47,7 @@ public async Task ProcessRequest( return value; } else if (kvp.Value is Dictionary) - value = JsonConvert.SerializeObject(kvp.Value); + value = JsonSerializer.Serialize(kvp.Value, client.SerializerOptions); else return String.Format("{0}={1}", kvp.Key, kvp.Value); @@ -91,7 +90,7 @@ public async Task ProcessRequest( } ; - var serializedresult = JsonConvert.SerializeObject(BodyJson); + var serializedresult = JsonSerializer.Serialize(BodyJson, client.SerializerOptions); byte[] requestBody = Encoding.UTF8.GetBytes(serializedresult); StreamReader reader = null; HttpWebResponse response = null; diff --git a/Contentstack.Core/Internals/JsonNodeConversion.cs b/Contentstack.Core/Internals/JsonNodeConversion.cs new file mode 100644 index 00000000..97bd7f4a --- /dev/null +++ b/Contentstack.Core/Internals/JsonNodeConversion.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Contentstack.Core.Internals +{ + /// + /// Converts trees to CLR types similar to Newtonsoft + /// ToObject<Dictionary<string, object>> (nested objects and collections). + /// + internal static class JsonNodeConversion + { + internal static Dictionary JsonObjectToDictionary(JsonObject obj) + { + var dict = new Dictionary(StringComparer.Ordinal); + if (obj == null) + { + return dict; + } + + foreach (var kvp in obj) + { + dict[kvp.Key] = JsonNodeToClr(kvp.Value); + } + + return dict; + } + + internal static object JsonNodeToClr(JsonNode node) + { + if (node == null) + { + return null; + } + + if (node is JsonObject jo) + { + return JsonObjectToDictionary(jo); + } + + if (node is JsonArray ja) + { + var list = new List(ja.Count); + foreach (var item in ja) + { + list.Add(JsonNodeToClr(item)); + } + return list; + } + + if (node is JsonValue jv) + { + return JsonValueToClr(jv); + } + + return null; + } + + private static object JsonValueToClr(JsonValue jv) + { + var el = JsonSerializer.Deserialize(jv.ToJsonString()); + return JsonElementToObject(el); + } + + private static object JsonElementToObject(JsonElement el) + { + switch (el.ValueKind) + { + case JsonValueKind.Object: + var o = new Dictionary(StringComparer.Ordinal); + foreach (var p in el.EnumerateObject()) + { + o[p.Name] = JsonElementToObject(p.Value); + } + return o; + case JsonValueKind.Array: + var list = new List(); + foreach (var item in el.EnumerateArray()) + { + list.Add(JsonElementToObject(item)); + } + return list; + case JsonValueKind.String: + return el.GetString(); + case JsonValueKind.Number: + if (el.TryGetInt32(out var i32)) + { + return i32; + } + if (el.TryGetInt64(out var i64)) + { + return i64; + } + if (el.TryGetDouble(out var d)) + { + return d; + } + return el.GetRawText(); + case JsonValueKind.True: + return true; + case JsonValueKind.False: + return false; + case JsonValueKind.Null: + case JsonValueKind.Undefined: + default: + return null; + } + } + } +} diff --git a/Contentstack.Core/Internals/JsonObjectMerge.cs b/Contentstack.Core/Internals/JsonObjectMerge.cs new file mode 100644 index 00000000..ab4487e5 --- /dev/null +++ b/Contentstack.Core/Internals/JsonObjectMerge.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; + +namespace Contentstack.Core.Internals +{ + /// + /// Replaces Newtonsoft JObject.Merge for query objects in . + /// + internal static class JsonObjectMerge + { + /// + /// Merges into (modifies ). + /// For array values, appends items from that are not already present + /// (string compare of JSON text), similar to Newtonsoft MergeArrayHandling.Union. + /// + internal static void UnionMergeInto(JsonObject target, JsonObject addition) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + if (addition == null) + { + return; + } + + foreach (var kvp in addition) + { + if (!target.TryGetPropertyValue(kvp.Key, out var existing) || existing == null) + { + target[kvp.Key] = kvp.Value?.DeepClone(); + continue; + } + + if (existing is JsonObject existingObj && kvp.Value is JsonObject addObj) + { + UnionMergeInto(existingObj, addObj); + } + else if (existing is JsonArray existingArr && kvp.Value is JsonArray addArr) + { + var present = new HashSet(StringComparer.Ordinal); + foreach (var n in existingArr) + { + if (n != null) + { + present.Add(n.ToJsonString()); + } + } + foreach (var n in addArr) + { + if (n == null) + { + continue; + } + var s = n.ToJsonString(); + if (present.Add(s)) + { + existingArr.Add(n.DeepClone()); + } + } + } + else + { + target[kvp.Key] = kvp.Value?.DeepClone(); + } + } + } + } +} diff --git a/Contentstack.Core/Internals/StackOutput.cs b/Contentstack.Core/Internals/StackOutput.cs index 235acc11..493aeddd 100644 --- a/Contentstack.Core/Internals/StackOutput.cs +++ b/Contentstack.Core/Internals/StackOutput.cs @@ -6,6 +6,24 @@ namespace Contentstack.Core.Internals { internal class StackOutput { + internal StackOutput() + { + // Touch fields so CS0414 does not fire for legacy/reflection-only state retained for compatibility tests. + _ = _TotalCount; + _ = _Json; + _ = _Notice; + _ = _Output; + _ = _ObjectAttributes; + _ = _Schema; + _ = _Object; + _ = _Objects; + _ = _Result; + _ = _ApplicationUser; + _ = _Tags; + _ = _Owner; + _ = _Uid; + } + private Int32 _TotalCount = 0; private string _Json = string.Empty; private string _Notice = string.Empty; diff --git a/Contentstack.Core/Models/Asset.cs b/Contentstack.Core/Models/Asset.cs index d3a18037..713db74b 100644 --- a/Contentstack.Core/Models/Asset.cs +++ b/Contentstack.Core/Models/Asset.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Collections.Generic; using System.IO; using System.Linq; @@ -7,8 +8,9 @@ using Contentstack.Core.Configuration; using Contentstack.Core.Internals; using Contentstack.Utils.Interfaces; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace Contentstack.Core.Models { @@ -64,7 +66,7 @@ public object this[string key] { if (e.Source != null) { - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } } } @@ -98,19 +100,19 @@ public string Url /// /// This is Asset type uid. /// - [JsonProperty(propertyName: "_content_type_uid")] + [JsonPropertyName("_content_type_uid")] public string ContentTypeUid { get; set; } /// /// The size of the file in bytes. /// - [JsonProperty(PropertyName = "file_size")] + [JsonPropertyName("file_size")] public string FileSize { get; set; } /// /// The original name of the file. /// - [JsonProperty(PropertyName = "filename")] + [JsonPropertyName("filename")] public string FileName { get; set; } /// @@ -121,7 +123,7 @@ public string Url /// /// Localized title of the asset (e.g. when fetched with SetLocale / asset localisation). /// - [JsonProperty(PropertyName = "title")] + [JsonPropertyName("title")] public string Title { get; set; } /// @@ -132,50 +134,50 @@ public string Url /// /// This content_type in asset. /// - [JsonProperty(propertyName: "content_type")] + [JsonPropertyName("content_type")] public string ContentType { get; set; } /// /// This for whether it is asset directory /// - [JsonProperty(propertyName: "is_dir")] + [JsonPropertyName("is_dir")] public Boolean IsDir { get; set; } /// /// Uid of user who updated the file /// - [JsonProperty(PropertyName = "updated_by")] + [JsonPropertyName("updated_by")] public string UpdatedBy { get; set; } /// /// Uid of user who updated the file /// - [JsonProperty(PropertyName = "created_by")] + [JsonPropertyName("created_by")] public string CreatedBy { get; set; } /// /// The Uid of folder in which the asset is present /// - [JsonProperty(PropertyName = "parent_uid")] + [JsonPropertyName("parent_uid")] public string ParentUid { get; set; } /// /// The Version of Asset /// - [JsonProperty(PropertyName = "_version")] - public string Version { get; set; } + [JsonPropertyName("_version")] + public int Version { get; set; } /// /// Dimension Object of the asset containing Height and width /// - [JsonProperty(PropertyName = "dimension")] + [JsonPropertyName("dimension")] public Dictionary Dimension { get; set; } /// /// Dimension Object of the asset publish details /// - [JsonProperty(PropertyName = "publish_details")] + [JsonPropertyName("publish_details")] public Dictionary PublishDetails { get; set; } #region Internal Constructors @@ -192,6 +194,7 @@ internal Asset(ContentstackClient stack) this._StackHeaders = stack._LocalHeaders; } + [JsonConstructor] internal Asset() { @@ -355,9 +358,9 @@ public void RemoveHeader(string key) } - internal void ParseObject(JObject jsonObj) + internal void ParseObject(JsonObject jsonObj) { - this._ObjectAttributes = jsonObj.ToObject>(); + this._ObjectAttributes = JsonNodeConversion.JsonObjectToDictionary(jsonObj); } public DateTime GetCreateAt() @@ -371,7 +374,7 @@ public DateTime GetCreateAt() catch (Exception e) { if (e.Source != null) - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } return DateTime.MinValue; } @@ -411,7 +414,7 @@ public DateTime GetUpdateAt() catch (Exception e) { if (e.Source != null) - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } return DateTime.MinValue; } @@ -433,7 +436,7 @@ public DateTime GetDeleteAt() catch (Exception e) { if (e.Source != null) - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } return DateTime.MinValue; } @@ -468,8 +471,8 @@ public async Task Fetch() { HttpRequestHandler RequestHandler = new HttpRequestHandler(this.StackInstance); var outputResult = await RequestHandler.ProcessRequest(_Url, headers, mainJson, Branch: this.StackInstance.Config.Branch, timeout: this.StackInstance.Config.Timeout, proxy: this.StackInstance.Config.Proxy); - JObject obj = JObject.Parse(ContentstackConvert.ToString(outputResult, "{}")); - return obj.SelectToken("$.asset").ToObject(this.StackInstance.Serializer); + JsonObject obj = JsonNode.Parse(ContentstackConvert.ToString(outputResult, "{}"))!.AsObject(); + return JsonSerializer.Deserialize(obj["asset"]!.ToJsonString(), this.StackInstance.SerializerOptions); } catch (Exception ex) { @@ -545,21 +548,7 @@ internal static ContentstackException GetContentstackError(Exception ex) using (var reader = new StreamReader(stream)) { errorMessage = reader.ReadToEnd(); - JObject data = JObject.Parse(errorMessage.Replace("\r\n", "")); - //errorCode = ContentstackConvert.ToInt32(data.Property("error_code").Value); - //errorMessage = ContentstackConvert.ToString(data.Property("error_message").Value); - - JToken token = data["error_code"]; - if (token != null) - errorCode = token.Value(); - - token = data["error_message"]; - if (token != null) - errorMessage = token.Value(); - - token = data["errors"]; - if (token != null) - errors = token.ToObject>(); + ApiErrorBodyParser.TryApply(errorMessage.Replace("\r\n", ""), ref errorCode, ref errorMessage, ref errors); var response = exResp as HttpWebResponse; if (response != null) diff --git a/Contentstack.Core/Models/AssetLibrary.cs b/Contentstack.Core/Models/AssetLibrary.cs index 6a51f744..cbc7d327 100644 --- a/Contentstack.Core/Models/AssetLibrary.cs +++ b/Contentstack.Core/Models/AssetLibrary.cs @@ -6,7 +6,8 @@ using System.Threading.Tasks; using Contentstack.Core.Configuration; using Contentstack.Core.Internals; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; namespace Contentstack.Core.Models { @@ -75,35 +76,31 @@ public void SortWithKeyAndOrderBy(String key, OrderBy order) /// /// ContentstackClient stack = new ContentstackClinet("api_key", "delivery_token", "environment"); /// AssetLibrary assetLibrary = stack.AssetLibrary(); - /// JObject jObject = await assetLibrary.Count(); + /// JsonObject jObject = await assetLibrary.Count(); /// /// - public async Task Count() + public async Task Count() { UrlQueries.Add("count", "true"); return await Exec(); } - public AssetLibrary Query(JObject QueryObject) + public AssetLibrary Query(JsonObject QueryObject) { try { if (UrlQueries.ContainsKey("query")) { // If query already exists, append/merge the new query object - JObject existingQuery = UrlQueries["query"] as JObject; + JsonObject existingQuery = UrlQueries["query"] as JsonObject; if (existingQuery != null) { - // Merge the new query object with the existing one - existingQuery.Merge(QueryObject, new JsonMergeSettings - { - MergeArrayHandling = MergeArrayHandling.Union - }); + JsonObjectMerge.UnionMergeInto(existingQuery, QueryObject); UrlQueries["query"] = existingQuery; } else { - // If existing query is not a JObject, replace it + // If existing query is not a JsonObject, replace it UrlQueries["query"] = QueryObject; } } @@ -147,7 +144,7 @@ public AssetLibrary Where(string key, string value) if (UrlQueries.ContainsKey("query")) { // If query already exists, get it and add the key-value pair - JObject existingQuery = UrlQueries["query"] as JObject; + JsonObject existingQuery = UrlQueries["query"] as JsonObject; if (existingQuery != null) { existingQuery[key] = value; @@ -155,8 +152,8 @@ public AssetLibrary Where(string key, string value) } else { - // If existing query is not a JObject, create a new one - JObject newQuery = new JObject(); + // If existing query is not a JsonObject, create a new one + JsonObject newQuery = new JsonObject(); newQuery[key] = value; UrlQueries["query"] = newQuery; } @@ -164,7 +161,7 @@ public AssetLibrary Where(string key, string value) else { // If query doesn't exist, create a new one with the key-value pair - JObject newQuery = new JObject(); + JsonObject newQuery = new JsonObject(); newQuery[key] = value; UrlQueries.Add("query", newQuery); } @@ -549,10 +546,15 @@ public AssetLibrary RemoveHeader(string key) /// public async Task> FetchAll() { - JObject json = await Exec(); - var assets = json.SelectToken("$.assets") - .ToObject>(this.Stack.Serializer); - var collection = json.ToObject>(this.Stack.Serializer); + JsonObject json = await Exec(); + JsonArray assetsArray = json["assets"]?.AsArray(); + IEnumerable assets = Enumerable.Empty(); + if (assetsArray != null) + { + assets = JsonSerializer.Deserialize>(assetsArray.ToJsonString(), this.Stack.SerializerOptions) + ?? new List(); + } + var collection = ContentstackCollection.FromDeliveryEnvelope(json, assets); foreach (var entry in assets) { if (entry.GetType() == typeof(Asset)) @@ -560,11 +562,10 @@ public async Task> FetchAll() (entry as Asset).SetStackInstance(this.Stack); } } - collection.Items = assets; return collection; } - private async Task Exec() + private async Task Exec() { Dictionary headers = GetHeader(_Headers); @@ -596,7 +597,7 @@ private async Task Exec() timeout: this.Stack.Config.Timeout, proxy: this.Stack.Config.Proxy ); - return JObject.Parse(ContentstackConvert.ToString(outputResult, "{}")); + return JsonNode.Parse(ContentstackConvert.ToString(outputResult, "{}"))!.AsObject(); } catch (Exception ex) { @@ -671,19 +672,7 @@ internal static ContentstackException GetContentstackError(Exception ex) using (var reader = new StreamReader(stream)) { errorMessage = reader.ReadToEnd(); - JObject data = JObject.Parse(errorMessage.Replace("\r\n", "")); - - JToken token = data["error_code"]; - if (token != null) - errorCode = token.Value(); - - token = data["error_message"]; - if (token != null) - errorMessage = token.Value(); - - token = data["errors"]; - if (token != null) - errors = token.ToObject>(); + ApiErrorBodyParser.TryApply(errorMessage.Replace("\r\n", ""), ref errorCode, ref errorMessage, ref errors); var response = exResp as HttpWebResponse; if (response != null) diff --git a/Contentstack.Core/Models/ContentType.cs b/Contentstack.Core/Models/ContentType.cs index a87f7c9d..16716f6b 100644 --- a/Contentstack.Core/Models/ContentType.cs +++ b/Contentstack.Core/Models/ContentType.cs @@ -4,10 +4,9 @@ using Contentstack.Core.Internals; using System.Threading.Tasks; using System.Net; -using Newtonsoft.Json.Linq; using System.Linq; using System.IO; -using Newtonsoft.Json; +using System.Text.Json.Nodes; namespace Contentstack.Core.Models { @@ -82,19 +81,7 @@ internal static ContentstackException GetContentstackError(Exception ex) using (var reader = new StreamReader(stream)) { errorMessage = reader.ReadToEnd(); - JObject data = JObject.Parse(errorMessage.Replace("\r\n", "")); - - JToken token = data["error_code"]; - if (token != null) - errorCode = token.Value(); - - token = data["error_message"]; - if (token != null) - errorMessage = token.Value(); - - token = data["errors"]; - if (token != null) - errors = token.ToObject>(); + ApiErrorBodyParser.TryApply(errorMessage.Replace("\r\n", ""), ref errorCode, ref errorMessage, ref errors); var response = exResp as HttpWebResponse; if (response != null) @@ -138,7 +125,7 @@ internal void SetStackInstance(ContentstackClient stack) /// /// is dictionary of additional parameter /// The Content-Type Schema Object. - public async Task Fetch(Dictionary param = null) + public async Task Fetch(Dictionary param = null) { Dictionary headers = GetHeader(_Headers); Dictionary headerAll = new Dictionary(); @@ -169,8 +156,8 @@ public async Task Fetch(Dictionary param = null) { HttpRequestHandler RequestHandler = new HttpRequestHandler(this.StackInstance); var outputResult = await RequestHandler.ProcessRequest(_Url, headers, mainJson, Branch: this.StackInstance.Config.Branch, timeout: this.StackInstance.Config.Timeout, proxy: this.StackInstance.Config.Proxy); - JObject data = JsonConvert.DeserializeObject(outputResult.Replace("\r\n", ""), this.StackInstance.SerializerSettings); - JObject contentTypes = (Newtonsoft.Json.Linq.JObject)data["content_type"]; + JsonObject data = JsonNode.Parse(outputResult.Replace("\r\n", ""))!.AsObject(); + JsonObject contentTypes = data["content_type"]!.AsObject(); return contentTypes; } catch (Exception ex) diff --git a/Contentstack.Core/Models/ContentstackCollection.cs b/Contentstack.Core/Models/ContentstackCollection.cs index 524c4b12..73d286d3 100644 --- a/Contentstack.Core/Models/ContentstackCollection.cs +++ b/Contentstack.Core/Models/ContentstackCollection.cs @@ -1,12 +1,63 @@ -using Newtonsoft.Json; -using System.Collections; +using System.Collections; using System.Collections.Generic; +using System.Globalization; +using System.Text.Json.Nodes; namespace Contentstack.Core.Models { - [JsonObject] public class ContentstackCollection : IEnumerable { + /// + /// Builds a collection from a Delivery / Management API envelope object. + /// (Do not use JsonSerializer on this type: it implements IEnumerable and STJ would expect a JSON array at the root.) + /// + internal static ContentstackCollection FromDeliveryEnvelope(JsonObject envelope, IEnumerable items) + { + static JsonNode FindProperty(JsonObject obj, params string[] candidateNames) + { + var wanted = new HashSet(candidateNames, System.StringComparer.OrdinalIgnoreCase); + foreach (var kv in obj) + { + if (wanted.Contains(kv.Key)) + return kv.Value; + } + return null; + } + + static int ReadInt(JsonObject obj, params string[] candidateNames) + { + var node = FindProperty(obj, candidateNames); + if (node == null) + return 0; + try + { + return node.GetValue(); + } + catch + { + try + { + var s = node.ToString()?.Trim(); + if (string.IsNullOrEmpty(s)) + return 0; + return int.Parse(s, CultureInfo.InvariantCulture); + } + catch + { + return 0; + } + } + } + + return new ContentstackCollection + { + Items = items, + Skip = ReadInt(envelope, "skip"), + Limit = ReadInt(envelope, "limit"), + Count = ReadInt(envelope, "count", "total_count"), + }; + } + /// /// The number of items skipped in this resultset. /// diff --git a/Contentstack.Core/Models/Entry.cs b/Contentstack.Core/Models/Entry.cs index 70d1948d..dc0af276 100644 --- a/Contentstack.Core/Models/Entry.cs +++ b/Contentstack.Core/Models/Entry.cs @@ -1,5 +1,4 @@ using Markdig; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; @@ -8,7 +7,9 @@ using System.Threading.Tasks; using Contentstack.Core.Internals; using Contentstack.Core.Configuration; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace Contentstack.Core.Models { @@ -24,7 +25,7 @@ public class Entry private CachePolicy _CachePolicy; private Dictionary UrlQueries = new Dictionary(); private bool _IsCachePolicySet; - private JObject jObject; + private JsonObject jObject; private string _Url { get @@ -113,7 +114,7 @@ private string _Url /// /// Dimension Object of the entries publish details /// - [JsonProperty(PropertyName = "publish_details")] + [JsonPropertyName("publish_details")] public Dictionary PublishDetails { get; set; } /// @@ -159,6 +160,7 @@ public Dictionary Object #endregion #region Internal Constructors + [JsonConstructor] internal Entry() { } @@ -190,19 +192,7 @@ internal static ContentstackException GetContentstackError(Exception ex) using (var reader = new StreamReader(stream)) { errorMessage = reader.ReadToEnd(); - JObject data = JObject.Parse(errorMessage.Replace("\r\n", "")); - - JToken token = data["error_code"]; - if (token != null) - errorCode = token.Value(); - - token = data["error_message"]; - if (token != null) - errorMessage = token.Value(); - - token = data["errors"]; - if (token != null) - errors = token.ToObject>(); + ApiErrorBodyParser.TryApply(errorMessage.Replace("\r\n", ""), ref errorCode, ref errorMessage, ref errors); var response = exResp as HttpWebResponse; if (response != null) @@ -867,7 +857,7 @@ public String GetDeletedBy() /// }); /// /// - public JObject ToJson() + public JsonObject ToJson() { return this.jObject; } @@ -892,7 +882,7 @@ public JObject ToJson() private Asset GetAsset(String key) { - JObject assetObject = (JObject)jObject.GetValue(key); + JsonObject assetObject = jObject[key]!.AsObject(); var asset = ContentTypeInstance.StackInstance.Asset(); asset.ParseObject(assetObject); return asset; @@ -917,12 +907,11 @@ private Asset GetAsset(String key) private List GetAssets(String key) { List assets = new List(); - JArray assetArray = (Newtonsoft.Json.Linq.JArray)jObject.GetValue(key); - //Dictionary assetArray = (Dictionary)_ObjectAttributes[key]; + JsonArray assetArray = jObject[key]!.AsArray(); - foreach (JToken v in assetArray) + foreach (JsonNode v in assetArray) { - JObject assetobj = (JObject)v; + JsonObject assetobj = v.AsObject(); Asset asset = ContentTypeInstance.StackInstance.Asset(); asset.ParseObject(assetobj); assets.Add(asset); @@ -1401,47 +1390,13 @@ public async Task Fetch() //Dictionary urlQueries = new Dictionary(); - var livePreviewConfig = this.ContentTypeInstance?.StackInstance?.LivePreviewConfig; - if (livePreviewConfig != null - && livePreviewConfig.Enable - && livePreviewConfig.PreviewResponse != null - && livePreviewConfig.PreviewResponse.Type == JTokenType.Object - && livePreviewConfig.PreviewResponse.HasValues - && !string.IsNullOrEmpty(this.Uid) - && string.Equals(livePreviewConfig.EntryUID, this.Uid, StringComparison.Ordinal) - && this.ContentTypeInstance != null - && string.Equals( - livePreviewConfig.ContentTypeUID, - this.ContentTypeInstance.ContentTypeId, - StringComparison.OrdinalIgnoreCase) - && livePreviewConfig.IsCachedPreviewForCurrentQuery()) - { - try - { - var serializedFromPreview = livePreviewConfig.PreviewResponse.ToObject( - this.ContentTypeInstance.StackInstance.Serializer); - if (serializedFromPreview != null && serializedFromPreview.GetType() == typeof(Entry)) - { - (serializedFromPreview as Entry).ContentTypeInstance = this.ContentTypeInstance; - } - return serializedFromPreview; - } - catch - { - // Fall through to network fetch. - } - } - if (headers != null && headers.Count() > 0) { foreach (var header in headers) { - if (this.ContentTypeInstance != null - && livePreviewConfig != null - && livePreviewConfig.Enable - && livePreviewConfig.ContentTypeUID == this.ContentTypeInstance.ContentTypeId - && header.Key == "access_token" - && !string.IsNullOrEmpty(livePreviewConfig.LivePreview)) + if (this.ContentTypeInstance.StackInstance.LivePreviewConfig.Enable == true + && this.ContentTypeInstance.StackInstance.LivePreviewConfig.ContentTypeUID == this.ContentTypeInstance.ContentTypeId + && header.Key == "access_token" && !string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.LivePreview)) { continue; } @@ -1449,36 +1404,25 @@ public async Task Fetch() } } bool isLivePreview = false; - var hasLivePreviewContext = - this.ContentTypeInstance != null - && livePreviewConfig != null - && livePreviewConfig.Enable - && livePreviewConfig.ContentTypeUID == this.ContentTypeInstance.ContentTypeId - && ( - !string.IsNullOrEmpty(livePreviewConfig.LivePreview) - || !string.IsNullOrEmpty(livePreviewConfig.ReleaseId) - || !string.IsNullOrEmpty(livePreviewConfig.PreviewTimestamp) - ); - - if (hasLivePreviewContext) + if (this.ContentTypeInstance.StackInstance.LivePreviewConfig.Enable == true && this.ContentTypeInstance.StackInstance.LivePreviewConfig.ContentTypeUID == this.ContentTypeInstance.ContentTypeId) { - mainJson.Add("live_preview", string.IsNullOrEmpty(livePreviewConfig.LivePreview)? "init" : livePreviewConfig.LivePreview); + mainJson.Add("live_preview", string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.LivePreview)? "init" : this.ContentTypeInstance.StackInstance.LivePreviewConfig.LivePreview); - if (!string.IsNullOrEmpty(livePreviewConfig.ManagementToken)) { - headerAll["authorization"] = livePreviewConfig.ManagementToken; - } else if (!string.IsNullOrEmpty(livePreviewConfig.PreviewToken)) { - headerAll["preview_token"] = livePreviewConfig.PreviewToken; + if (!string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.ManagementToken)) { + headerAll["authorization"] = this.ContentTypeInstance.StackInstance.LivePreviewConfig.ManagementToken; + } else if (!string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.PreviewToken)) { + headerAll["preview_token"] = this.ContentTypeInstance.StackInstance.LivePreviewConfig.PreviewToken; } else { throw new LivePreviewException(); } - if (!string.IsNullOrEmpty(livePreviewConfig.ReleaseId)) + if (!string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.ReleaseId)) { - headerAll["release_id"] = livePreviewConfig.ReleaseId; + headerAll["release_id"] = this.ContentTypeInstance.StackInstance.LivePreviewConfig.ReleaseId; } - if (!string.IsNullOrEmpty(livePreviewConfig.PreviewTimestamp)) + if (!string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.PreviewTimestamp)) { - headerAll["preview_timestamp"] = livePreviewConfig.PreviewTimestamp; + headerAll["preview_timestamp"] = this.ContentTypeInstance.StackInstance.LivePreviewConfig.PreviewTimestamp; } isLivePreview = true; @@ -1500,8 +1444,8 @@ public async Task Fetch() HttpRequestHandler RequestHandler = new HttpRequestHandler(this.ContentTypeInstance.StackInstance); var outputResult = await RequestHandler.ProcessRequest(_Url, headerAll, mainJson, Branch: this.ContentTypeInstance.StackInstance.Config.Branch, isLivePreview: isLivePreview, timeout: this.ContentTypeInstance.StackInstance.Config.Timeout, proxy: this.ContentTypeInstance.StackInstance.Config.Proxy); - JObject obj = JObject.Parse(ContentstackConvert.ToString(outputResult, "{}")); - var serializedObject = obj.SelectToken("$.entry").ToObject(this.ContentTypeInstance.StackInstance.Serializer); + JsonObject obj = JsonNode.Parse(ContentstackConvert.ToString(outputResult, "{}"))!.AsObject(); + var serializedObject = JsonSerializer.Deserialize(obj["entry"]!.ToJsonString(), this.ContentTypeInstance.StackInstance.SerializerOptions); if (serializedObject.GetType() == typeof(Entry)) { (serializedObject as Entry).ContentTypeInstance = this.ContentTypeInstance; @@ -1567,24 +1511,14 @@ private Dictionary GetHeader(Dictionary localHea } } - internal void ParseObject(JObject jsonObj, string url = null) + internal void ParseObject(JsonObject jsonObj, string url = null) { this.jObject = jsonObj; - this._ObjectAttributes = jsonObj.ToObject>(); - if (_ObjectAttributes != null && _ObjectAttributes.ContainsKey("_metadata")) + this._ObjectAttributes = JsonNodeConversion.JsonObjectToDictionary(jsonObj); + if (_ObjectAttributes != null && _ObjectAttributes.TryGetValue("_metadata", out var mdObj) + && mdObj is Dictionary mdDict) { - var jObject = (Newtonsoft.Json.Linq.JObject)_ObjectAttributes["_metadata"]; - var _metadataJSON = new Dictionary(); - foreach (var property in jObject.Properties()) - { - _metadataJSON[property.Name] = property.Value.ToObject(); - } - List iterator = _metadataJSON.Keys.ToList(); - Metadata = new Dictionary(); - foreach (var key in iterator) - { - Metadata.Add(key, _metadataJSON[key]); - } + Metadata = new Dictionary(mdDict); } } diff --git a/Contentstack.Core/Models/GlobalField.cs b/Contentstack.Core/Models/GlobalField.cs index 46c39438..f36b8bd5 100644 --- a/Contentstack.Core/Models/GlobalField.cs +++ b/Contentstack.Core/Models/GlobalField.cs @@ -4,10 +4,9 @@ using Contentstack.Core.Internals; using System.Threading.Tasks; using System.Net; -using Newtonsoft.Json.Linq; using System.Linq; using System.IO; -using Newtonsoft.Json; +using System.Text.Json.Nodes; namespace Contentstack.Core.Models { @@ -96,19 +95,7 @@ internal static ContentstackException GetContentstackError(Exception ex) using (var reader = new StreamReader(stream)) { errorMessage = reader.ReadToEnd(); - JObject data = JObject.Parse(errorMessage.Replace("\r\n", "")); - - JToken token = data["error_code"]; - if (token != null) - errorCode = token.Value(); - - token = data["error_message"]; - if (token != null) - errorMessage = token.Value(); - - token = data["errors"]; - if (token != null) - errors = token.ToObject>(); + ApiErrorBodyParser.TryApply(errorMessage.Replace("\r\n", ""), ref errorCode, ref errorMessage, ref errors); var response = exResp as HttpWebResponse; if (response != null) @@ -152,7 +139,7 @@ internal void SetStackInstance(ContentstackClient stack) /// /// A dictionary of additional parameters. /// The Global Field schema object. - public async Task Fetch(Dictionary param = null) + public async Task Fetch(Dictionary param = null) { Dictionary headers = GetHeader(_Headers); Dictionary headerAll = new Dictionary(); @@ -181,9 +168,9 @@ public async Task Fetch(Dictionary param = null) { HttpRequestHandler RequestHandler = new HttpRequestHandler(this.StackInstance); var outputResult = await RequestHandler.ProcessRequest(_Url, headers, mainJson, Branch: this.StackInstance.Config.Branch, timeout: this.StackInstance.Config.Timeout, proxy: this.StackInstance.Config.Proxy); - JObject data = JsonConvert.DeserializeObject(outputResult.Replace("\r\n", ""), this.StackInstance.SerializerSettings); + JsonObject data = JsonNode.Parse(outputResult.Replace("\r\n", ""))!.AsObject(); - JObject globalTypes = (Newtonsoft.Json.Linq.JObject)data["global_field"]; + JsonObject globalTypes = data["global_field"]!.AsObject(); return globalTypes; } catch (Exception ex) diff --git a/Contentstack.Core/Models/GlobalFieldQuery.cs b/Contentstack.Core/Models/GlobalFieldQuery.cs index d804c1a9..f11a50ef 100644 --- a/Contentstack.Core/Models/GlobalFieldQuery.cs +++ b/Contentstack.Core/Models/GlobalFieldQuery.cs @@ -4,10 +4,10 @@ using Contentstack.Core.Internals; using System.Threading.Tasks; using System.Net; -using Newtonsoft.Json.Linq; using System.Linq; using System.IO; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Nodes; namespace Contentstack.Core.Models { @@ -66,19 +66,7 @@ internal static ContentstackException GetContentstackError(Exception ex) using (var reader = new StreamReader(stream)) { errorMessage = reader.ReadToEnd(); - JObject data = JObject.Parse(errorMessage.Replace("\r\n", "")); - - JToken token = data["error_code"]; - if (token != null) - errorCode = token.Value(); - - token = data["error_message"]; - if (token != null) - errorMessage = token.Value(); - - token = data["errors"]; - if (token != null) - errors = token.ToObject>(); + ApiErrorBodyParser.TryApply(errorMessage.Replace("\r\n", ""), ref errorCode, ref errorMessage, ref errors); var response = exResp as HttpWebResponse; if (response != null) @@ -122,7 +110,7 @@ internal void SetStackInstance(ContentstackClient stack) /// /// A dictionary of additional parameters. /// The global field schema object. - public async Task Find(Dictionary param = null) + public async Task Find(Dictionary param = null) { Dictionary headers = GetHeader(_Headers); Dictionary headerAll = new Dictionary(); @@ -150,7 +138,7 @@ public async Task Find(Dictionary param = null) { HttpRequestHandler RequestHandler = new HttpRequestHandler(this.StackInstance); var outputResult = await RequestHandler.ProcessRequest(_Url, headers, mainJson, Branch: this.StackInstance.Config.Branch, timeout: this.StackInstance.Config.Timeout, proxy: this.StackInstance.Config.Proxy); - JObject data = JsonConvert.DeserializeObject(outputResult.Replace("\r\n", ""), this.StackInstance.SerializerSettings); + JsonObject data = JsonNode.Parse(outputResult.Replace("\r\n", ""))!.AsObject(); return data; } diff --git a/Contentstack.Core/Models/Query.cs b/Contentstack.Core/Models/Query.cs index 09dcc9c8..7e2608d2 100644 --- a/Contentstack.Core/Models/Query.cs +++ b/Contentstack.Core/Models/Query.cs @@ -1,5 +1,7 @@ -using Newtonsoft.Json.Linq; using System; +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Collections.Generic; using System.IO; using System.Linq; @@ -93,19 +95,7 @@ internal static ContentstackException GetContentstackError(Exception ex) using (var reader = new StreamReader(stream)) { errorMessage = reader.ReadToEnd(); - JObject data = JObject.Parse(errorMessage.Replace("\r\n", "")); - - JToken token = data["error_code"]; - if (token != null) - errorCode = token.Value(); - - token = data["error_message"]; - if (token != null) - errorMessage = token.Value(); - - token = data["errors"]; - if (token != null) - errors = token.ToObject>(); + ApiErrorBodyParser.TryApply(errorMessage.Replace("\r\n", ""), ref errorCode, ref errorMessage, ref errors); var response = exResp as HttpWebResponse; if (response != null) @@ -1267,7 +1257,7 @@ public Query Only(String[] fieldUid) } catch (Exception e) { - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } return this; @@ -1336,7 +1326,7 @@ public Query Except(String[] fieldUids) } catch (Exception e) { - Console.WriteLine(ErrorMessages.FormatExceptionDetails(e)); + Debug.WriteLine(ErrorMessages.FormatExceptionDetails(e)); } return this; } @@ -1798,7 +1788,7 @@ public async Task> Find() /// }); /// /// - public async Task Count() + public async Task Count() { try { @@ -1839,72 +1829,80 @@ public async Task> FindOne() return this.parseJObject(await Exec()); } - private ContentstackCollection parseJObject(JObject jObject) + private ContentstackCollection parseJObject(JsonObject jObject) { - - if(this.TaxonomyInstance!=null) + JsonSerializerOptions opts = this.TaxonomyInstance != null + ? this.TaxonomyInstance.SerializerOptions + : this.ContentTypeInstance.StackInstance.SerializerOptions; + + JsonArray entriesArray = jObject["entries"]?.AsArray(); + IEnumerable entries = Enumerable.Empty(); + + try + { + if (entriesArray != null) + { + entries = JsonSerializer.Deserialize>(entriesArray.ToJsonString(), opts) + ?? new List(); + } + } + catch (JsonException ex) { - var entries = jObject.SelectToken("$.entries").ToObject>(this.TaxonomyInstance.Serializer); - var collection = jObject.ToObject>(this.TaxonomyInstance.Serializer); - collection.Items = entries; - return collection; + var preview = jObject.ToJsonString(); + if (preview.Length > 500) + { + preview = preview.Substring(0, 500) + "..."; + } + + throw new ContentstackException( + "Could not deserialize entry documents from the query response. Response preview: " + preview, + ex); + } + + var collection = ContentstackCollection.FromDeliveryEnvelope(jObject, entries); - } else + if (this.TaxonomyInstance == null) { - var entries = jObject.SelectToken("$.entries").ToObject>(this.ContentTypeInstance.StackInstance.Serializer); - var collection = jObject.ToObject>(this.ContentTypeInstance.StackInstance.Serializer); foreach (var entry in entries) { - if (entry.GetType() == typeof(Entry)) + if (entry != null && entry.GetType() == typeof(Entry)) { (entry as Entry).SetContentTypeInstance(this.ContentTypeInstance); } } - collection.Items = entries; - return collection; } - + + return collection; } #endregion - private async Task Exec() + private async Task Exec() { Dictionary headers = GetHeader(_Headers); Dictionary headerAll = new Dictionary(); Dictionary mainJson = new Dictionary(); bool isLivePreview = false; - var livePreviewConfig = this.ContentTypeInstance?.StackInstance?.LivePreviewConfig; - var hasLivePreviewContext = - this.ContentTypeInstance != null - && livePreviewConfig != null - && livePreviewConfig.Enable - && livePreviewConfig.ContentTypeUID == this.ContentTypeInstance.ContentTypeId - && ( - !string.IsNullOrEmpty(livePreviewConfig.LivePreview) - || !string.IsNullOrEmpty(livePreviewConfig.ReleaseId) - || !string.IsNullOrEmpty(livePreviewConfig.PreviewTimestamp) - ); - - if (hasLivePreviewContext) - { - mainJson.Add("live_preview", string.IsNullOrEmpty(livePreviewConfig.LivePreview) ? "init" : livePreviewConfig.LivePreview); - - if (!string.IsNullOrEmpty(livePreviewConfig.ManagementToken)) { - headerAll["authorization"] = livePreviewConfig.ManagementToken; - } else if (!string.IsNullOrEmpty(livePreviewConfig.PreviewToken)) { - headerAll["preview_token"] = livePreviewConfig.PreviewToken; + if (this.ContentTypeInstance!=null && this.ContentTypeInstance.StackInstance.LivePreviewConfig.Enable == true + && this.ContentTypeInstance.StackInstance?.LivePreviewConfig.ContentTypeUID == this.ContentTypeInstance.ContentTypeId) + { + mainJson.Add("live_preview", string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.LivePreview) ? "init" : this.ContentTypeInstance.StackInstance.LivePreviewConfig.LivePreview); + + if (!string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.ManagementToken)) { + headerAll["authorization"] = this.ContentTypeInstance.StackInstance.LivePreviewConfig.ManagementToken; + } else if (!string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.PreviewToken)) { + headerAll["preview_token"] = this.ContentTypeInstance.StackInstance.LivePreviewConfig.PreviewToken; } else { throw new LivePreviewException(); } - if (!string.IsNullOrEmpty(livePreviewConfig.ReleaseId)) + if (!string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.ReleaseId)) { - headerAll["release_id"] = livePreviewConfig.ReleaseId; + headerAll["release_id"] = this.ContentTypeInstance.StackInstance.LivePreviewConfig.ReleaseId; } - if (!string.IsNullOrEmpty(livePreviewConfig.PreviewTimestamp)) + if (!string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.PreviewTimestamp)) { - headerAll["preview_timestamp"] = livePreviewConfig.PreviewTimestamp; + headerAll["preview_timestamp"] = this.ContentTypeInstance.StackInstance.LivePreviewConfig.PreviewTimestamp; } isLivePreview = true; @@ -1914,11 +1912,10 @@ private async Task Exec() { foreach (var header in headers) { - if (this.ContentTypeInstance!=null && livePreviewConfig != null - && livePreviewConfig.Enable - && livePreviewConfig.ContentTypeUID == this.ContentTypeInstance?.ContentTypeId + if (this.ContentTypeInstance!=null && this.ContentTypeInstance?.StackInstance.LivePreviewConfig.Enable == true + && this.ContentTypeInstance?.StackInstance.LivePreviewConfig.ContentTypeUID == this.ContentTypeInstance?.ContentTypeId && header.Key == "access_token" - && !string.IsNullOrEmpty(livePreviewConfig.LivePreview)) + && !string.IsNullOrEmpty(this.ContentTypeInstance.StackInstance.LivePreviewConfig.LivePreview)) { continue; } @@ -1969,13 +1966,13 @@ private async Task Exec() HttpRequestHandler requestHandler = new HttpRequestHandler(this.TaxonomyInstance); var branch = this.TaxonomyInstance.Config.Branch != null ? this.TaxonomyInstance.Config.Branch : "main"; var outputResult = await requestHandler.ProcessRequest(this._Url, headerAll, mainJson, Branch: branch, isLivePreview: isLivePreview, timeout: this.TaxonomyInstance.Config.Timeout); - return JObject.Parse(ContentstackConvert.ToString(outputResult, "{}")); + return JsonNode.Parse(ContentstackConvert.ToString(outputResult, "{}"))!.AsObject(); } else { HttpRequestHandler requestHandler = new HttpRequestHandler(this.ContentTypeInstance.StackInstance); var outputResult = await requestHandler.ProcessRequest(_Url, headerAll, mainJson, Branch: this.ContentTypeInstance.StackInstance.Config.Branch, isLivePreview: isLivePreview, timeout: this.ContentTypeInstance.StackInstance.Config.Timeout); - return JObject.Parse(ContentstackConvert.ToString(outputResult, "{}")); + return JsonNode.Parse(ContentstackConvert.ToString(outputResult, "{}"))!.AsObject(); } } catch (Exception ex) diff --git a/Contentstack.Core/Models/SyncStack.cs b/Contentstack.Core/Models/SyncStack.cs index bd531dce..54d2b94d 100644 --- a/Contentstack.Core/Models/SyncStack.cs +++ b/Contentstack.Core/Models/SyncStack.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using Newtonsoft.Json; - +using System.Text.Json.Serialization; + namespace Contentstack.Core.Models -{ +{ /// /// Represents the result of a sync operation. /// @@ -15,17 +15,17 @@ public class SyncStack /// /// Readonly property to check totalCount /// - [JsonProperty("total_count")] - public int TotalCount { get; set; } + [JsonPropertyName("total_count")] + public int TotalCount { get; set; } /// /// Readonly property to delta sync. /// - [JsonProperty("sync_token")] - public string SyncToken { get; set; } + [JsonPropertyName("sync_token")] + public string SyncToken { get; set; } /// /// Readonly property for paginating sync /// - [JsonProperty("pagination_token")] + [JsonPropertyName("pagination_token")] public string PaginationToken { get; set;} } } \ No newline at end of file diff --git a/Contentstack.Core/Models/Taxonomy.cs b/Contentstack.Core/Models/Taxonomy.cs index 9d7322f0..667915df 100644 --- a/Contentstack.Core/Models/Taxonomy.cs +++ b/Contentstack.Core/Models/Taxonomy.cs @@ -4,8 +4,6 @@ using System.Net; using Contentstack.Core.Configuration; using Contentstack.Core.Internals; -using Newtonsoft.Json.Linq; - namespace Contentstack.Core.Models { public class Taxonomy: Query @@ -34,7 +32,7 @@ protected override string _Url } } #endregion - public ContentstackClient Stack + public new ContentstackClient Stack { get; set; @@ -254,7 +252,7 @@ private Dictionary GetHeader(Dictionary localHea return _StackHeaders; } } - internal static ContentstackException GetContentstackError(Exception ex) + internal static new ContentstackException GetContentstackError(Exception ex) { Int32 errorCode = 0; string errorMessage = string.Empty; @@ -280,27 +278,7 @@ internal static ContentstackException GetContentstackError(Exception ex) if (!string.IsNullOrWhiteSpace(errorMessage)) { - try - { - JObject data = JObject.Parse(errorMessage.Replace("\r\n", "")); - - JToken token = data["error_code"]; - if (token != null) - errorCode = token.Value(); - - token = data["error_message"]; - if (token != null) - errorMessage = token.Value(); - - token = data["errors"]; - if (token != null) - errors = token.ToObject>(); - } - catch (Newtonsoft.Json.JsonException) - { - // If JSON parsing fails, use the raw error message - // errorMessage is already set from ReadToEnd() - } + ApiErrorBodyParser.TryApply(errorMessage.Replace("\r\n", ""), ref errorCode, ref errorMessage, ref errors); } var response = exResp as HttpWebResponse; diff --git a/Directory.Build.props b/Directory.Build.props index 79401e83..1f737f6c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 2.28.0 + 3.0.0 diff --git a/README.md b/README.md index a07ff77c..01f0a749 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,17 @@ .NET SDK for Contentstack's Content Delivery API -Contributor and agent conventions: see **[AGENTS.md](AGENTS.md)**. - ## Getting Started This guide will help you get started with our .NET SDK to build apps powered by Contentstack. +## Prerequisites +To get started with .NET, you will need the following: + +- .NET 10 or later + +> **Migrating from v2.x?** Version 3.0.0 replaces Newtonsoft.Json with System.Text.Json and changes several public API return/parameter types (e.g. `Entry.ToJson()`, `Query.Count()` now return `JsonObject` instead of `JObject`). See the [migration guide](https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/dot-net/migrate-dotnet-delivery-sdk-from-newtonsoft-to-stj) before upgrading. + ## SDK Installation and Setup To use the .NET SDK, download it from here @@ -101,6 +106,31 @@ query.Find().ContinueWith((t) => { } }); ``` +## Endpoint Resolution + +Use the `Endpoint` class to resolve Contentstack service URLs for any supported region without hardcoding hosts. This is useful when configuring `ContentstackOptions` for a specific region, including the newly added `gcp-eu` region. + +``` cs +using Contentstack.Core.Endpoints; // Endpoint + +// Resolve the Content Delivery endpoint for a region +string url = Endpoint.GetContentstackEndpoint("us", "contentDelivery"); +// → "https://cdn.contentstack.io" + +// GCP-EU region support +string gcpEuUrl = Endpoint.GetContentstackEndpoint("gcp-eu", "contentDelivery"); + +// Strip the https:// scheme — useful when passing the host directly to SDK configuration +var options = new ContentstackOptions() +{ + ApiKey = "", + DeliveryToken = "", + Environment = "", + Host = Endpoint.GetContentstackEndpoint("eu", "contentDelivery", omitHttps: true) +}; +ContentstackClient stack = new ContentstackClient(options); +``` + ## API Reference Go through our .NET SDK API Reference guide to know about the methods that can be used to query your content in Contentstack. diff --git a/Scripts/refresh-region.py b/Scripts/refresh-region.py new file mode 100644 index 00000000..b4a5fc3c --- /dev/null +++ b/Scripts/refresh-region.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# Refresh regions.json from the Contentstack CDN. +# Usage (run from your project root): +# python3 Scripts/refresh-region.py +# python Scripts/refresh-region.py + +import glob +import json +import os +import ssl +import sys +import urllib.request + +REGIONS_URL = "https://artifacts.contentstack.com/regions.json" + +print(f"Fetching {REGIONS_URL} ...") + + +def _fetch(url): + # First attempt: normal SSL verification + try: + with urllib.request.urlopen(url, timeout=30) as r: + return r.read().decode("utf-8") + except urllib.error.URLError as e: + if "CERTIFICATE_VERIFY_FAILED" not in str(e): + raise + # macOS python.org builds often lack system certs — retry without verification + print("WARNING: SSL certificate verification failed. Retrying without verification.", file=sys.stderr) + print(" To fix permanently, run: /Applications/Python*/Install\\ Certificates.command", file=sys.stderr) + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + with urllib.request.urlopen(url, timeout=30, context=ctx) as r: + return r.read().decode("utf-8") + + +try: + raw = _fetch(REGIONS_URL) +except Exception as e: + print(f"ERROR: Could not download regions.json: {e}", file=sys.stderr) + sys.exit(1) + +try: + data = json.loads(raw) +except json.JSONDecodeError as e: + print(f"ERROR: Downloaded content is not valid JSON: {e}", file=sys.stderr) + sys.exit(1) + +if "regions" not in data: + print("ERROR: Downloaded JSON does not contain a 'regions' key.", file=sys.stderr) + sys.exit(1) + +region_count = len(data["regions"]) + +# Scan bin/ for every copy of the DLL (covers Debug/Release, any TFM, any nesting). +dll_pattern = os.path.join(os.getcwd(), "**", "Contentstack.Core.dll") +found = [ + os.path.dirname(dll) + for dll in glob.glob(dll_pattern, recursive=True) + if os.sep + "bin" + os.sep in dll +] + +if not found: + print("[bin] No build output found — run 'dotnet build' first, then re-run this script.") + sys.exit(1) + +for bin_dir in found: + assets_dir = os.path.join(bin_dir, "Assets") + os.makedirs(assets_dir, exist_ok=True) + dest = os.path.join(assets_dir, "regions.json") + with open(dest, "w", encoding="utf-8") as f: + f.write(raw) + print(f"[bin] Wrote {region_count} regions → {dest}") diff --git a/Scripts/requirements.txt b/Scripts/requirements.txt deleted file mode 100644 index 377b0d3e..00000000 --- a/Scripts/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Secure XML parsing (fixes Snyk CWE-611 Insecure Xml Parser / XXE) -defusedxml>=0.7.1 diff --git a/build/contentstack.csharp.targets b/build/contentstack.csharp.targets new file mode 100644 index 00000000..94d5abbf --- /dev/null +++ b/build/contentstack.csharp.targets @@ -0,0 +1,14 @@ + + + <_RefreshScriptDest>$(MSBuildProjectDirectory)/Scripts/refresh-region.py + <_RefreshScriptSrc>$(MSBuildThisFileDirectory)../contentFiles/cs/any/Scripts/refresh-region.py + + + + + + + + diff --git a/skills/README.md b/skills/README.md deleted file mode 100644 index f3e91cdd..00000000 --- a/skills/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Skills – Contentstack .NET SDK - -Source of truth for detailed guidance. Read **[AGENTS.md](../AGENTS.md)** first, then open the skill that matches your task. - -## When to use which skill - -| Skill folder | Use when | -|--------------|----------| -| `dev-workflow` | Building the solution, versioning, CI workflows, onboarding, PR prep. | -| `sdk-core-patterns` | Architecture, `ContentstackClient`, HTTP layer, DI, plugins, request flow. | -| `query-building` | Query operators, fluent API, pagination, sync, taxonomy, `Query.cs`. | -| `models-and-serialization` | Entry/Asset models, JSON converters, `ContentstackCollection`, serialization. | -| `error-handling` | Exceptions, `ErrorMessages`, parsing API errors. | -| `testing` | Writing or debugging unit/integration tests, coverage, test layout. | -| `code-review` | Reviewing a PR against SDK-specific checklist. | - -Each folder contains **`SKILL.md`** with YAML frontmatter (`name`, `description`) for agent discovery. - -### Cursor - -In Cursor, you can also reference a skill in chat with `@skills/` (for example `@skills/testing`). diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md deleted file mode 100644 index 9ea79041..00000000 --- a/skills/code-review/SKILL.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -name: code-review -description: SDK-specific PR review checklist for the Contentstack .NET SDK — covers breaking changes, HTTP layer correctness, exception handling, serialization, fluent API patterns, configuration, test coverage, multi-targeting, plugin lifecycle, and internal visibility. Use when reviewing pull requests, examining code changes, or performing code quality assessments on this SDK. ---- - -# Code Review - -## When to use - -- Reviewing a PR or diff against SDK conventions. -- Self-review before opening a PR. - -## Severity levels - -- **Critical** — Must fix before merge (correctness, breaking changes, security) -- **Important** — Should fix (maintainability, SDK patterns, consistency) -- **Suggestion** — Consider improving (style, optimization) - -The sections below provide category-by-category checklists (breaking changes, HTTP, exceptions, serialization, fluent API, config, tests, multi-targeting, plugins, visibility) and a short red-flag list for quick scanning. - - -## Code Review Checklist - -### Breaking Changes Checklist - -```markdown -## Breaking Changes Review -- [ ] No public method signatures removed or changed without [Obsolete] deprecation -- [ ] No [JsonProperty] values changed (would break consumer deserialization silently) -- [ ] No ContentstackOptions public property removed -- [ ] New required options have defaults (don't break existing consumers who don't set them) -- [ ] No namespace renames without backward-compatible type aliases -- [ ] No IContentstackPlugin interface signature changed -- [ ] Version bump planned if breaking change is intentional (Directory.Build.props) -``` - -### HTTP Layer Checklist - -```markdown -## HTTP Layer Review -- [ ] All HTTP calls route through HttpRequestHandler.ProcessRequest -- [ ] No HttpClient instantiation anywhere in the PR -- [ ] New query params added to UrlQueries dict (not directly to URL string) -- [ ] New field-level filters added to QueryValueJson dict -- [ ] New headers added via Headers parameter to ProcessRequest -- [ ] Branch header uses "branch" key, passed as separate Branch parameter -- [ ] No hardcoded URLs — BaseUrl comes from Config.BaseUrl -- [ ] Live preview URL resolved via Config.getBaseUrl() — not hardcoded -- [ ] ProcessRequest result (string JSON) parsed, not further HTTP calls made -``` - -### Exception Handling Checklist - -```markdown -## Exception Handling Review -- [ ] Domain-specific exception type used (QueryFilterException, AssetException, etc.) -- [ ] No bare `throw new Exception(...)` or `throw new ContentstackException(...)` -- [ ] All message strings sourced from ErrorMessages.cs constants -- [ ] No string literals in throw statements -- [ ] GetContentstackError(ex) called when catching WebException from HTTP calls -- [ ] ErrorCode, StatusCode, Errors preserved when re-wrapping exceptions -- [ ] New domain area has new exception class with factory methods -- [ ] New error messages added to correct section in ErrorMessages.cs -- [ ] FormatExceptionDetails(innerEx) used in ProcessingError factory methods -``` - -### Serialization Checklist - -```markdown -## Serialization Review -- [ ] All public properties mapping CDA JSON fields have [JsonProperty("snake_case")] -- [ ] No reliance on default Newtonsoft.Json camelCase or PascalCase matching -- [ ] Custom deserialization uses [CSJsonConverter] + JsonConverter subclass -- [ ] JsonConverter placed in Contentstack.Core/Internals/ (internal class) -- [ ] No System.Text.Json usage -- [ ] No JsonConvert.DeserializeObject with hardcoded type outside of converter -- [ ] ContentstackCollection used for list responses (not List directly) -- [ ] "entries" token used for entry collection, "assets" for asset collection -``` - -### Fluent API Checklist - -```markdown -## Fluent API Review -- [ ] Every Query filter/operator method returns `return this;` -- [ ] Null key validated at start of method → QueryFilterException.Create() -- [ ] Empty string key validated → QueryFilterException.Create() -- [ ] Operator value stored in QueryValueJson[key][$operator] nested dict -- [ ] URL-level params stored in UrlQueries[key] -- [ ] Method name follows verb+noun pattern (GreaterThan, ContainedIn, NotExists) -- [ ] No mutation of QueryValueJson or UrlQueries outside of the Query class itself -- [ ] And()/Or() accept Query[] (not raw dictionaries) -``` - -### Configuration Checklist - -```markdown -## Configuration Review -- [ ] New options added to ContentstackOptions (public class), not Config (internal) -- [ ] New property has XML doc comment -- [ ] Default value set in ContentstackOptions() constructor or property initializer -- [ ] ContentstackClient constructor reads new option and passes to Config -- [ ] Config never exposed as public property -- [ ] New option tested in ContentstackOptionsUnitTests.cs -- [ ] ASP.NET Core binding works (IOptions path verified) -``` - -### Test Coverage Checklist - -```markdown -## Test Coverage Review -- [ ] Unit test for each new public Query method (QueryValueJson assertion via reflection) -- [ ] Unit test for null key input → QueryFilterException -- [ ] Unit test for empty key input → QueryFilterException -- [ ] Unit test for fluent return (Assert.Equal(query, result)) -- [ ] Integration test file in Integration/{FeatureName}Tests/ subfolder -- [ ] Integration test class extends IntegrationTestBase -- [ ] Integration test constructor takes ITestOutputHelper output -- [ ] CreateClient() used (not manual ContentstackClient construction) -- [ ] LogArrange/LogAct/LogAssert used in correct order -- [ ] TestAssert.* used (not raw Assert.*) -- [ ] [Fact(DisplayName = "FeatureArea - Component Description")] present -- [ ] Happy path test (valid params → expected response) -- [ ] Error path test (invalid params or not found → expected exception) -``` - -### Multi-Targeting Checklist - -```markdown -## Multi-Targeting Review -- [ ] No HttpClient (netstandard2.0 HttpClient has behavioural differences from net4x) -- [ ] No System.Text.Json (not available without separate package in netstandard2.0) -- [ ] No record types (C# 9, requires LangVersion setting for net47/net472) -- [ ] No default interface implementations (C# 8, may affect net47) -- [ ] No nullable reference types without #nullable enable guard -- [ ] No top-level statements (not applicable to library projects but worth checking) -- [ ] Tested compile against netstandard2.0 target (or verified via CI) -``` - -### Plugin Lifecycle Checklist - -```markdown -## Plugin Lifecycle Review -- [ ] New feature that makes HTTP calls uses HttpRequestHandler (plugins run automatically) -- [ ] No WebRequest.Create() called directly in new model classes -- [ ] IContentstackPlugin interface not modified (breaking for all plugin consumers) -- [ ] RequestLoggingPlugin still works with any new request/response changes -- [ ] Plugin.OnRequest receives HttpWebRequest before send -- [ ] Plugin.OnResponse receives response string (can mutate/inspect) -``` - -### Internal Visibility Checklist - -```markdown -## Internal Visibility Review -- [ ] New utility/helper classes in Internals/ are marked `internal` -- [ ] New model types intended for consumers are in Models/ and `public` -- [ ] New configuration types are in Configuration/ and `public` -- [ ] No public exposure of Config, HttpRequestHandler, or VersionUtility -- [ ] InternalsVisibleTo not modified (already covers both test projects) -- [ ] New internal methods accessible in unit tests without changes -``` - -### Common Issues Found in Past PRs - -#### Silent Deserialization Failures -`[JsonProperty]` omitted → field is always null at runtime, no exception. Verify all properties that map CDA JSON fields. - -#### Exception Message in Throw -```csharp -// Bad -throw new QueryFilterException("Please provide valid params."); - -// Good -throw QueryFilterException.Create(innerEx); -// or -throw new QueryFilterException(ErrorMessages.QueryFilterError); -``` - -#### Hardcoded Environment -```csharp -// Bad — breaks for consumers with different environments -mainJson["environment"] = "production"; - -// Correct — already done in Exec() -mainJson["environment"] = ContentTypeInstance.StackInstance.Config.Environment; -``` - -#### Returning void from Query Method -```csharp -// Bad — breaks fluent chaining -public void SetMyParam(string value) { UrlQueries["my_param"] = value; } - -// Good -public Query SetMyParam(string value) { UrlQueries["my_param"] = value; return this; } -``` - -#### Dictionary Not Initialized for QueryValueJson Entry -```csharp -// Bad — throws KeyNotFoundException or InvalidCastException -((Dictionary)QueryValueJson[key])["$op"] = value; - -// Good — guard with ContainsKey -if (!QueryValueJson.ContainsKey(key)) - QueryValueJson[key] = new Dictionary(); -((Dictionary)QueryValueJson[key])["$op"] = value; -``` - -## SDK-specific red flags - -Quick scan for anti-patterns in PRs: - -``` -❌ new HttpClient() — use HttpRequestHandler -❌ throw new Exception("message") — use typed ContentstackException subclass -❌ "hardcoded_field_name" — use [JsonProperty] or ErrorMessages constant -❌ public Config GetConfig() — Config is internal by design -❌ return void — Query methods return Query (fluent) -❌ [JsonProperty] omitted — CDA uses snake_case; PascalCase won't deserialize -❌ in .csproj — use Directory.Build.props -``` - -For full category-by-category review, see **Code review checklist** above. diff --git a/skills/dev-workflow/SKILL.md b/skills/dev-workflow/SKILL.md deleted file mode 100644 index d0378b04..00000000 --- a/skills/dev-workflow/SKILL.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -name: dev-workflow -description: Local development and CI workflow for the Contentstack .NET SDK — solution layout, dotnet build/test commands, package versioning in Directory.Build.props, GitHub Actions (unit tests, CodeQL, NuGet), and integration test credentials. Use when onboarding, running builds, preparing a PR, or finding where CI is defined. ---- - -# Dev workflow – Contentstack .NET SDK - -## When to use - -- Onboarding, building, or running tests locally. -- Finding which workflow applies to PRs, releases, or security scans. -- Maintainer tasks: DocFX, NuGet publish expectations. - -## Invariants - -- Version in **`Directory.Build.props`** only for package version. -- Integration tests need **`app.config`** (or equivalent) — do not commit secrets. - -## Repo tooling — CI, release, docs, security - -### Local commands - -| Action | Command | -|--------|---------| -| Build | `dotnet build Contentstack.Net.sln` | -| Unit tests | `dotnet test Contentstack.Core.Unit.Tests/Contentstack.Core.Unit.Tests.csproj` | -| Integration tests | `dotnet test Contentstack.Core.Tests/Contentstack.Core.Tests.csproj` (credentials) | - -Package version: [`Directory.Build.props`](../../../Directory.Build.props) (``). - -### CI — unit tests - -[`.github/workflows/unit-test.yml`](../../../.github/workflows/unit-test.yml): on `pull_request` and `push`, Windows runner, .NET 7, `dotnet restore` → `dotnet build Contentstack.Net.sln` → `dotnet test` on `Contentstack.Core.Unit.Tests`. - -### CI — branch and release flow - -Release PRs follow the direct flow `development -> master` (or `main` where applicable); `staging` is no longer part of the release path. Keep feature work targeting `development`, and use the back-merge workflow to keep branches aligned after production releases. - -### CI — CodeQL - -[`.github/workflows/codeql-analysis.yml`](../../../.github/workflows/codeql-analysis.yml): static analysis on PRs (language matrix includes C# as configured in the workflow). - -### Release — NuGet - -[`.github/workflows/nuget-publish.yml`](../../../.github/workflows/nuget-publish.yml): triggered on **`release` `created`**. - -- `dotnet pack -c Release -o out` -- Push `contentstack.csharp.*.nupkg` to NuGet.org (`NUGET_API_KEY` / `NUGET_AUTH_TOKEN` secrets — names appear in workflow; values are org secrets). -- Secondary job may push to GitHub Packages (`nuget.pkg.github.com`). - -Maintainers: ensure `Directory.Build.props` version matches the release tag policy before publishing. - -### DocFX API docs - -Configuration: [`docfx_project/docfx.json`](../../../docfx_project/docfx.json). Output site: `_site/` under the docfx project. - -Prerequisite: [DocFX](https://dotnet.github.io/docfx/) CLI installed. - -```bash -cd docfx_project -docfx docfx.json -# or: docfx build docfx.json -``` - -If metadata `src` paths do not resolve (e.g. `src/**.csproj`), adjust `docfx.json` or project layout so metadata generation matches this repository’s structure. - -Related: [`filterRules.yml`](../../../docfx_project/filterRules.yml) for API filter rules. - -### Security / policy automation - -**SCA (dependencies):** [`.github/workflows/sca-scan.yml`](../../../.github/workflows/sca-scan.yml) — on PR, `dotnet restore`, then Snyk Dotnet action against `Contentstack.Core/obj/project.assets.json` (`SNYK_TOKEN` secret). Failures indicate vulnerable packages or scan misconfiguration. - -**Policy (repository hygiene):** [`.github/workflows/policy-scan.yml`](../../../.github/workflows/policy-scan.yml) — for public repos, checks presence of `SECURITY.md` (or `.github/SECURITY.md`) and a license file. Adjust repo contents if these jobs fail. - -When a PR fails these jobs, inspect the workflow log and fix dependencies or policy items as required by your team. - -### Projects (layout) - -| Path | Role | -|------|------| -| `Contentstack.Net.sln` | Main solution | -| `Contentstack.Core/` | Delivery SDK package | -| `Contentstack.AspNetCore/` | DI package | -| `Contentstack.Core.Unit.Tests/` | Unit tests | -| `Contentstack.Core.Tests/` | Integration tests | - -## Related skills - -- [SDK core patterns](../sdk-core-patterns/SKILL.md) — architecture and HTTP. -- [Testing](../testing/SKILL.md) — unit vs integration patterns and credentials. diff --git a/skills/error-handling/SKILL.md b/skills/error-handling/SKILL.md deleted file mode 100644 index b4b08a52..00000000 --- a/skills/error-handling/SKILL.md +++ /dev/null @@ -1,298 +0,0 @@ ---- -name: error-handling -description: Error handling patterns for the Contentstack .NET SDK — ContentstackException hierarchy, domain-specific typed exceptions with static factory methods, GetContentstackError WebException parsing, centralized ErrorMessages.cs strings, and consumer catch order. Use when adding new exception types for a new domain area, modifying error messages, handling API HTTP errors, debugging WebException responses, or catching SDK errors in consumer code. ---- - -# Error Handling - -## When to use - -- New exceptions, new `ErrorMessages` strings, or HTTP error parsing. -- Reviewing catches in SDK internals or consumer apps. - -## Invariants - -- Throw **domain-specific** subclasses of `ContentstackException`, not raw `ContentstackException` for domain cases. -- **All user-facing message strings** live in **`ErrorMessages.cs`** — no inline strings in `throw`. -- On HTTP errors, use **`GetContentstackError`** and preserve **`ErrorCode`**, **`StatusCode`**, **`Errors`** when re-throwing. - -## Exception hierarchy (summary) - -`ContentstackException` → `QueryFilterException`, `AssetException`, `LivePreviewException`, `GlobalFieldException`, `EntryException`, `TaxonomyException`, `ContentTypeException` (see `Contentstack.Core/Internals/ContentstackExceptions.cs`). - -Base properties: `ErrorCode`, `StatusCode`, `Errors`. - -## Error Patterns Reference - -### Complete ErrorMessages Catalogue - -All strings in `Contentstack.Core/Internals/ErrorMessages.cs`: - -#### Query and Filter -```csharp -QueryFilterError = "Please provide valid params." -InvalidParamsError = "Invalid parameters provided. {0}" -``` - -#### Asset -```csharp -AssetJsonConversionError = "Failed to convert asset JSON. Please check the asset format and data integrity." -AssetProcessingError = "An error occurred while processing the asset. {0}" -AssetLibraryRequestError = "Exception in {0}: {1}\nStackTrace: {2}" -``` - -#### Entry -```csharp -EntryProcessingError = "An error occurred while processing the entry. {0}" -EntryUidRequired = "Please set entry uid." -EntryNotFoundInCache = "Entry is not present in cache" -``` - -#### Global Field -```csharp -GlobalFieldIdNullError = "GlobalFieldId required. This value cannot be null or empty, define it in your configuration." -GlobalFieldProcessingError = "An error occurred while processing the globalField. {0}" -GlobalFieldQueryError = "Global field query failed. Check your query syntax and field schema before retrying." -``` - -#### Live Preview -```csharp -LivePreviewTokenMissing = "Live Preview token missing. Add either a PreviewToken or a ManagementToken in the LivePreviewConfig." -``` - -#### Client Request -```csharp -ContentstackClientRequestError = "Contentstack client request failed. Check your network settings or request parameters and try again: {0}" -ContentstackSyncRequestError = "An error occurred while processing the Contentstack client request: {0}" -``` - -#### Taxonomy -```csharp -TaxonomyProcessingError = "An error occurred while processing the taxonomy operation: {0}" -``` - -#### Content Type -```csharp -ContentTypeProcessingError = "Content type processing failed. Verify the schema and ensure all required fields are configured." -``` - -#### Authentication and Configuration -```csharp -StackApiKeyRequired = "Stack api key can not be null." -AccessTokenRequired = "Access token can not be null." -EnvironmentRequired = "Environment can not be null." -AuthenticationNotPresent = "Authentication Not present." -ContentTypeNameRequired = "Please set contentType name." -``` - -#### JSON and Parsing -```csharp -InvalidJsonFormat = "Please provide valid JSON." -ParsingError = "Parsing Error." -``` - -#### Network and Server -```csharp -NoConnectionError = "Connection error" -ServerError = "Server interaction went wrong, Please try again." -NetworkUnavailable = "Network not available." -DefaultError = "Oops! Something went wrong. Please try again." -``` - -#### Cache -```csharp -SavingNetworkCallResponseForCache = "Error while saving network call response." -``` - -#### Initialization -```csharp -ContentstackDefaultMethodNotCalled = "You must called Contentstack.stack() first" -``` - -### CDA API Error Response Format - -The Contentstack CDA returns errors in this JSON format: - -```json -{ - "error_message": "The requested entry doesn't exist.", - "error_code": 141, - "errors": { - "field_name": ["validation message"] - } -} -``` - -`error_code` is Contentstack-specific (not HTTP status). Common codes: -- `141` — Entry not found -- `141` — Asset not found -- `109` — API key invalid -- `103` — Access token invalid -- `129` — Invalid query parameters - -### HTTP Status to Exception Mapping - -| HTTP Status | Typical cause | SDK behavior | -|------------|--------------|-------------| -| 400 | Invalid query params | `QueryFilterException` | -| 401 | Invalid API key / token | `ContentstackException` with StatusCode 401 | -| 404 | Entry / asset not found | `ContentstackException` with StatusCode 404 | -| 422 | Invalid field value | `ContentstackException` with `Errors` dict populated | -| 429 | Rate limit exceeded | `ContentstackException` with StatusCode 429 | -| 500 | Server error | `ContentstackException` with StatusCode 500 | - -### Where GetContentstackError Is Called - -The same static `GetContentstackError` method is replicated (acknowledged code smell) in: -- `Query` — wraps in `QueryFilterException` -- `Entry` — wraps in `EntryException` -- `ContentType` — wraps in `ContentTypeException` -- `Asset` — wraps in `AssetException` -- `AssetLibrary` — wraps in `AssetException` - -When adding a new model, follow the same pattern — copy `GetContentstackError` into the new class (or call the one from `Query` if same namespace/access level permits). - -### Exception with Errors Dictionary - -When the API returns field-level validation errors: - -```csharp -catch (ContentstackException ex) -{ - if (ex.Errors != null) - { - foreach (var field in ex.Errors) - { - // field.Key = field name, field.Value = error messages - Console.WriteLine($"Field '{field.Key}': {field.Value}"); - } - } -} -``` - -### LivePreviewException Trigger Conditions - -Thrown when `LivePreviewConfig.Enable = true` but neither `ManagementToken` nor `PreviewToken` is configured: - -```csharp -if (livePreviewConfig.Enable) -{ - if (string.IsNullOrEmpty(livePreviewConfig.ManagementToken) - && string.IsNullOrEmpty(livePreviewConfig.PreviewToken)) - throw new LivePreviewException(); // uses LivePreviewTokenMissing message -} -``` - -### GlobalFieldException.CreateForIdNull Trigger - -Thrown when `GlobalField(null)` or `GlobalField("")` is called — validated before any HTTP request: - -```csharp -public GlobalField GlobalField(string uid) -{ - if (string.IsNullOrEmpty(uid)) - throw GlobalFieldException.CreateForIdNull(); - // ... -} -``` - -### Adding Messages for New Features — Checklist - -1. Add `public const string` to the appropriate section in `ErrorMessages.cs` -2. Use `{0}` placeholder for `string.Format` when appending exception details -3. Add the factory method on the exception class using `ErrorMessages.FormatExceptionDetails(ex)` for processing errors -4. Never concatenate exception details manually — always use `FormatExceptionDetails()` - ---- - -### Static factory pattern (examples) - -Use factories — not `new XyzException("literal")`: - -```csharp -throw QueryFilterException.Create(innerException); -throw GlobalFieldException.CreateForIdNull(); -throw AssetException.CreateForJsonConversionError(); -throw AssetException.CreateForProcessingError(innerException); -throw EntryException.CreateForProcessingError(innerException); -throw TaxonomyException.CreateForProcessingError(innerException); -throw ContentTypeException.CreateForProcessingError(innerException); -``` - -### Adding a new domain exception - -1. Add the class in `ContentstackExceptions.cs` extending `ContentstackException` with static factories. -2. Add `public const string` entries in `ErrorMessages.cs`; use `{0}` when wrapping `FormatExceptionDetails(innerException)`. - -Example skeleton: - -```csharp -public class MyFeatureException : ContentstackException -{ - public MyFeatureException(string message) : base(message) { } - public MyFeatureException(string message, Exception innerException) : base(message, innerException) { } - - public static MyFeatureException CreateForProcessingError(Exception innerException) - { - return new MyFeatureException( - string.Format(ErrorMessages.MyFeatureProcessingError, - ErrorMessages.FormatExceptionDetails(innerException)), - innerException); - } -} -``` - -### GetContentstackError (implementation sketch) - -`WebException` responses are parsed into `ContentstackException` with `ErrorCode`, `StatusCode`, and `Errors`. The same helper pattern is duplicated on `Query`, `Entry`, `ContentType`, `Asset`, `AssetLibrary` — follow the existing copy when adding a new HTTP-calling model. - -```csharp -internal static ContentstackException GetContentstackError(Exception ex) -{ - var webEx = (WebException)ex; - using var stream = webEx.Response.GetResponseStream(); - string errorMessage = new StreamReader(stream).ReadToEnd(); - JObject data = JObject.Parse(errorMessage); - int errorCode = data["error_code"]?.Value() ?? 0; - HttpStatusCode statusCode = ((HttpWebResponse)webEx.Response).StatusCode; - var errors = data["errors"]?.ToObject>(); - return new ContentstackException(data["error_message"]?.Value()) - { - ErrorCode = errorCode, - StatusCode = statusCode, - Errors = errors - }; -} -``` - -### Standard catch block (SDK internals) - -Preserve `ErrorCode`, `StatusCode`, and `Errors` when re-throwing domain exceptions: - -```csharp -catch (Exception ex) -{ - ContentstackException error = GetContentstackError(ex); - throw new QueryFilterException(error.Message, ex) - { - ErrorCode = error.ErrorCode, - StatusCode = error.StatusCode, - Errors = error.Errors - }; -} -``` - -### Consumer catch order (example) - -```csharp -try { /* await query.Find() */ } -catch (QueryFilterException ex) { /* query validation */ } -catch (ContentstackException ex) { /* API / HTTP */ } -catch (Exception ex) { /* network / unknown */ } -``` - -### ErrorMessages.FormatExceptionDetails - -```csharp -ErrorMessages.FormatExceptionDetails(innerException) -``` diff --git a/skills/models-and-serialization/SKILL.md b/skills/models-and-serialization/SKILL.md deleted file mode 100644 index c7a49100..00000000 --- a/skills/models-and-serialization/SKILL.md +++ /dev/null @@ -1,248 +0,0 @@ ---- -name: models-and-serialization -description: Model and serialization patterns for the Contentstack .NET SDK — Entry/Asset shape, catch-all Object dictionary, generic Fetch/Find projections, CSJsonConverter attribute-driven converter registration, EntryJsonConverter/AssetJsonConverter, ContentstackCollection, JsonProperty for API name mismatches, entry variants. Use when adding new model types, writing JSON converters, working with entry variants, embedded assets, or modifying serialization settings. ---- - -# Models and Serialization - -## When to use - -- New models, converters, or `JsonProperty` mappings. -- Entry variants, embedded references, RTE/modular blocks behavior. -- Tuning `SerializerSettings`. - -## Invariants - -- CDA JSON is **snake_case** — use **`[JsonProperty("snake_case")]`** on mapped properties; do not rely on default PascalCase naming. -- Custom converters: **`[CSJsonConverter("Name")]`** on the model + **`JsonConverter`** implementation in **`Contentstack.Core/Internals/`**. -- New converters are registered automatically at client init (see **CSJsonConverter registration flow** below). -- Use **`Newtonsoft.Json`** only — not `System.Text.Json`. - -## Serialization Patterns Reference - -### CSJsonConverter Registration Flow - -At `ContentstackClient` construction time: - -``` -1. Scan all assemblies in current AppDomain -2. Find all types with [CSJsonConverter("ConverterName")] attribute -3. Find the JsonConverter class with matching name in Contentstack.Core.Internals -4. Instantiate it and add to SerializerSettings.Converters -5. All subsequent Fetch/Find calls use these converters -``` - -This means **converter registration is automatic** — adding the attribute and converter class is all that's required. - -### EntryJsonConverter Internals - -`EntryJsonConverter` handles the nested entry JSON structure from the CDA: - -```json -{ - "uid": "blt123", - "title": "My Entry", - "publish_details": { "environment": "production", "locale": "en-us", "time": "...", "user": "..." }, - "locale": "en-us", - "_metadata": { ... }, - "custom_field": "value", - "reference_field": [{ "uid": "blt456", "_content_type_uid": "blog" }] -} -``` - -The converter: -1. Reads the raw `JObject` -2. Maps known fields to strongly-typed properties (`Uid`, `Title`, `PublishDetails`, etc.) -3. Puts all remaining fields into `entry.Object` (the catch-all dictionary) - -### AssetJsonConverter Internals - -Similar pattern for assets — maps `uid`, `title`, `url`, `content_type`, `file_size`, `filename`, `tags` to typed properties; remaining fields to `asset.Object`. - -### parseJObject\ in Query - -After `HttpRequestHandler.ProcessRequest` returns a JSON string, `Query.parseJObject` does: - -```csharp -JObject jObject = JObject.Parse(responseString); - -// For entry queries -JArray entries = (JArray)jObject["entries"]; -collection.Items = entries.ToObject>(client.Serializer); -collection.Count = jObject["count"]?.Value() ?? 0; -collection.Skip = (int)UrlQueries.GetValueOrDefault("skip", 0); -collection.Limit = (int)UrlQueries.GetValueOrDefault("limit", 100); -``` - -The `"entries"` token name is fixed by the CDA response format. Asset library uses `"assets"`. - -### JsonProperty Mapping Reference - -Key mappings already in the codebase: - -| JSON field | C# property | Model | -|-----------|-------------|-------| -| `publish_details` | `PublishDetails` | `Entry` | -| `_metadata` | `Metadata` / `_metadata` | `Entry` | -| `content_type` | `ContentType` | `Asset` | -| `file_size` | `FileSize` | `Asset` | -| `filename` | `FileName` | `Asset` | -| `created_at` | `CreatedAt` | various | -| `updated_at` | `UpdatedAt` | various | -| `created_by` | `CreatedBy` | various | - -### Newtonsoft.Json Settings Defaults - -The SDK uses default `JsonSerializerSettings` with no special configuration unless consumers override `client.SerializerSettings`. This means: -- `NullValueHandling.Include` (nulls are included) -- No date format override (ISO 8601 by default) -- No contract resolver override (property names as-is, so `[JsonProperty]` is required) -- No type name handling - -### Handling Embedded RTE Items - -RTE (Rich Text Editor) fields with embedded entries/assets are processed via `contentstack.utils` NuGet package. The `Entry` model surfaces the raw RTE JSON; consumers call the utils library to resolve embedded references: - -```csharp -// RTE field value from entry.Object["rte_field"] -// Pass to contentstack.utils for resolution -Utils.RenderContent(content, entryEmbeds); -``` - -### Deep Reference Deserialization - -When `IncludeReference()` is called, the CDA returns nested objects inside the entry JSON. These are deserialized as nested dictionaries in `entry.Object` or as typed sub-objects when the consumer POCO uses `[JsonProperty]` on the reference field: - -```csharp -public class BlogPost -{ - [JsonProperty("author")] - public Author Author { get; set; } // auto-deserialized if CDA returns expanded ref -} -``` - -If the reference is not expanded (not included), it will be an array of `{"uid": "...", "_content_type_uid": "..."}` objects. - -### Modular Blocks Deserialization - -Modular blocks are returned as JSON arrays of objects, each with a `_content_type_uid` discriminator: - -```json -"modular_blocks": [ - { "block_a": { "field1": "value" } }, - { "block_b": { "field2": "value" } } -] -``` - -Map with a `List>` or use a custom converter with a discriminator switch on the first key. - -### ContentstackCollection Parsing Edge Cases - -- `count` field only present when `IncludeCount()` is set on the query -- `entries` array is present even when empty (`[]`), never `null` -- `skip` and `limit` in the response may differ from what was requested if the CDA has its own limits - ---- - -### Entry model shape (quick reference) - -```csharp -// Strongly-typed fields (typical) -entry.Uid; entry.Title; entry.Tags; entry.Metadata; entry.PublishDetails; - -// Catch-all for arbitrary content type fields -entry.Object // Dictionary -``` - -```csharp -var price = entry.Object["price"]; -var color = entry.Object["color"] as string; -``` - -### Fetch and Find with typed POCOs - -Prefer typed models over `entry.Object` for structured access: - -```csharp -public class Product -{ - [JsonProperty("title")] - public string Title { get; set; } - - [JsonProperty("price")] - public decimal Price { get; set; } - - [JsonProperty("uid")] - public string Uid { get; set; } -} - -var result = await client.ContentType("product").Query().Find(); -var product = await client.ContentType("product").Entry("uid").Fetch(); -``` - -Deserialization uses `client.Serializer` (from `client.SerializerSettings`). - -### JsonProperty — always map snake_case - -The CDA uses `snake_case`. Newtonsoft defaults to PascalCase property names. Always annotate: - -```csharp -[JsonProperty("publish_details")] -public object PublishDetails { get; set; } -``` - -Without `[JsonProperty]`, deserialization looks for the wrong JSON keys. - -### CSJsonConverter on models - -```csharp -[CSJsonConverter("EntryJsonConverter")] -public class Entry { ... } -``` - -Converters live in `Contentstack.Core/Internals/`. See [CSJsonConverter Registration Flow](#csjsonconverter-registration-flow). - -### ContentstackCollection shape - -```csharp -public class ContentstackCollection -{ - public IEnumerable Items { get; } - public int Count { get; } - public int Skip { get; } - public int Limit { get; } -} -``` - -Parsed from `"entries"` or `"assets"` in `Query.parseJObject` (see [parseJObject\ in Query](#parsejobjectt-in-query)). - -### Asset model (common properties) - -`Uid`, `Title`, `Url`, `ContentType`, `FileSize`, `FileName`, `Tags`, plus `Object` for other fields. `AssetJsonConverter` handles nested JSON; `AssetLibrary.FetchAll()` returns `ContentstackCollection`. - -### Entry variants - -```csharp -entry.SetVariant("variant_uid"); -var result = await entry.Fetch(); -``` - -Uses `_variant` and the `x-cs-variant-uid` header path as implemented on `Entry`. - -### Adding a new model type (checklist) - -1. Add class under `Contentstack.Core/Models/` with `[JsonProperty]` on API fields and optional `Object` catch-all. -2. If custom deserialization is required, add `internal class MyModelJsonConverter : JsonConverter` in `Internals/`, implement `ReadJson` / `WriteJson` as needed (delivery is typically read-heavy). -3. Mark the model with `[CSJsonConverter("MyModelJsonConverter")]`. -4. Expose via `ContentstackClient` factory methods if it is a first-class API surface. - -### SerializerSettings customization - -Consumers may adjust before calls: - -```csharp -client.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; -client.SerializerSettings.DateFormatString = "yyyy-MM-dd"; -``` - -`Serializer` is rebuilt from `SerializerSettings` when needed. diff --git a/skills/query-building/SKILL.md b/skills/query-building/SKILL.md deleted file mode 100644 index fa9bbd80..00000000 --- a/skills/query-building/SKILL.md +++ /dev/null @@ -1,312 +0,0 @@ ---- -name: query-building -description: Query building patterns for the Contentstack .NET SDK — two-dictionary architecture (QueryValueJson + UrlQueries), Mongo-style operators, fluent chaining, Exec() merge order, pagination (skip/limit), sync API pagination tokens, taxonomy query path. Use when adding query operators or filters, modifying Query.cs, working on pagination, debugging query parameter serialization, or building entry/asset queries. ---- - -# Query Building - -## When to use - -- Adding or changing operators on `Query`, `AssetLibrary`, or related fluent APIs. -- Debugging why a filter or URL param is wrong. -- Pagination, sync, or taxonomy entry queries. - -## Invariants - -- Field-level Mongo filters live in **`QueryValueJson`**; top-level API params (locale, skip, limit, includes, etc.) live in **`UrlQueries`**. -- **`Exec()`** merges `environment`, optional live-preview keys, then `query` (from `QueryValueJson`), then each `UrlQueries` entry—later keys override earlier ones. -- New filter methods return **`this`** and validate keys with **`QueryFilterException`** where appropriate. -- Never set **`environment`** manually in `UrlQueries`—`Exec()` injects it from `Config`. - -## Exec() merge order (compact) - -```csharp -mainJson["environment"] = stack.Config.Environment; -// live preview keys if enabled -if (QueryValueJson.Count > 0) - mainJson["query"] = QueryValueJson; -foreach (var kvp in UrlQueries) - mainJson[kvp.Key] = kvp.Value; -// → HttpRequestHandler.ProcessRequest(..., BodyJson) -``` - -## Query Patterns Reference - -### Quick reference: QueryValueJson operators - -| Method | Operator | Example JSON shape | -|--------|----------|-------------------| -| `Where(key, value)` | direct equality | `{"field": "value"}` | -| `NotEqualTo(key, value)` | `$ne` | `{"field":{"$ne":"x"}}` | -| `ContainedIn(key, values)` | `$in` | `{"field":{"$in":["a","b"]}}` | -| `NotContainedIn(key, values)` | `$nin` | `{"field":{"$nin":["a"]}}` | -| `Exists(key)` | `$exists: true` | `{"field":{"$exists":true}}` | -| `NotExists(key)` | `$exists: false` | `{"field":{"$exists":false}}` | -| `GreaterThan(key, value)` | `$gt` | `{"field":{"$gt":10}}` | -| `LessThan(key, value)` | `$lt` | `{"field":{"$lt":10}}` | -| `Regex(key, pattern)` | `$regex` | `{"field":{"$regex":"^blt"}}` | -| `And(queries[])` | `$and` | array of query objects | -| `Or(queries[])` | `$or` | array of query objects | - -### Quick reference: UrlQueries keys - -| Method | Key | Notes | -|--------|-----|-------| -| `SetLocale(locale)` | `locale` | prefer over obsolete `Language` enum | -| `Skip(n)` | `skip` | pagination offset | -| `Limit(n)` | `limit` | pagination page size | -| `IncludeSchema()` | `include_schema` | `true` | -| `IncludeCount()` | `include_count` | `true` | -| `Tags(tags[])` | `tags` | `string[]` → repeated param | -| `Only(fields[])` | `only[BASE][]` | field projection | -| `Except(fields[])` | `except[BASE][]` | field exclusion | -| `IncludeReference(key)` | `include[]` | reference expansion | - -### Extending Query.cs (new fluent methods) - -#### New Mongo-style filter operator - -```csharp -// Pattern used by all existing operators in Query.cs -public Query MyNewOperator(string key, object value) -{ - if (key == null) - throw QueryFilterException.Create(new ArgumentNullException(nameof(key))); - - if (!QueryValueJson.ContainsKey(key)) - QueryValueJson[key] = new Dictionary(); - - ((Dictionary)QueryValueJson[key])["$myop"] = value; - return this; // always return this for fluent chaining -} -``` - -#### New URL-level parameter - -```csharp -public Query MyParam(string value) -{ - UrlQueries["my_param"] = value; - return this; -} -``` - -### Terminal operations - -```csharp -Task> result = await query.Find(); -Task result = await query.FindOne(); -Task count = await query.Count(); -``` - -### Paginating Find results (entries) - -```csharp -// ContentstackCollection response shape -result.Items // IEnumerable -result.Count // total count (requires IncludeCount()) -result.Skip // current offset -result.Limit // current page size - -// Paging loop -int skip = 0, limit = 100; -ContentstackCollection page; -do { - page = await query.Skip(skip).Limit(limit).Find(); - // process page.Items - skip += limit; -} while (page.Items.Count() == limit); -``` - -### SyncStack short reference - -When using `SyncRecursive` / sync APIs: - -```csharp -SyncStack syncResult = await client.SyncRecursive(parameters); -// SyncStack.PaginationToken — non-null while more pages exist -// SyncStack.SyncToken — final token for next delta sync -``` - -(See [Sync API Patterns](#sync-api-patterns) below for full sync flows.) - -### Complete Operator Categories - -#### Comparison Operators - -```csharp -query.Where("title", "My Entry") // direct equality -query.NotEqualTo("status", "draft") // $ne -query.GreaterThan("price", 100) // $gt -query.GreaterThanEqualTo("price", 100) // $gte -query.LessThan("price", 200) // $lt -query.LessThanEqualTo("price", 200) // $lte -``` - -#### Array / Set Operators - -```csharp -query.ContainedIn("color", new[] {"red", "blue"}) // $in -query.NotContainedIn("color", new[] {"green"}) // $nin -``` - -#### Existence Operators - -```csharp -query.Exists("field_name") // $exists: true -query.NotExists("field_name") // $exists: false -``` - -#### String Operators - -```csharp -query.Regex("uid", "^blt[a-zA-Z0-9]+$") // $regex -query.Regex("title", "^hello", "i") // $regex with $options modifier -``` - -#### Logical Operators - -```csharp -// And / Or take Query[] — each sub-query builds its own QueryValueJson -var q1 = client.ContentType("ct").Query().Where("color", "red"); -var q2 = client.ContentType("ct").Query().Where("size", "large"); -query.And(new[] { q1, q2 }); // $and -query.Or(new[] { q1, q2 }); // $or -``` - -### Reference / Include Patterns - -```csharp -query.IncludeReference("reference_field"); // expand single reference -query.IncludeReference(new[] {"ref1", "ref2"}); // expand multiple -query.IncludeSchema(); // include content type schema -query.IncludeCount(); // include total count in response -query.IncludeOwner(); // include entry owner info -query.IncludeMetadata(); // include entry metadata -``` - -### Field Projection - -```csharp -query.Only(new[] {"title", "uid", "price"}); // return only these fields -query.Except(new[] {"body", "image"}); // exclude these fields - -// For referenced fields -query.OnlyWithReferenceUid(new[] {"title"}, "reference_field"); -query.ExceptWithReferenceUid(new[] {"body"}, "reference_field"); -``` - -### Ordering - -```csharp -query.OrderByAscending("title"); -query.OrderByDescending("created_at"); -``` - -### Locale and Environment - -```csharp -query.SetLocale("en-us"); // preferred — string locale code -// Environment is injected automatically from Config in Exec() -// Never set "environment" manually in UrlQueries -``` - -### Exec() Implementation Detail - -The full merge performed in `Query.Exec()` before calling `HttpRequestHandler`: - -```csharp -var mainJson = new Dictionary(); - -// 1. Environment (always injected from Config) -mainJson["environment"] = ContentTypeInstance.StackInstance.Config.Environment; - -// 2. Live preview headers (if enabled and content type matches) -if (livePreviewActive) -{ - mainJson["live_preview"] = livePreviewConfig.LivePreview; - mainJson["authorization"] = livePreviewConfig.ManagementToken; - // or mainJson["preview_token"] = livePreviewConfig.PreviewToken; -} - -// 3. Mongo-style query filter (only if non-empty) -if (QueryValueJson.Count > 0) - mainJson["query"] = QueryValueJson; - -// 4. All UrlQueries (locale, skip, limit, includes, projections, etc.) -foreach (var kvp in UrlQueries) - mainJson[kvp.Key] = kvp.Value; -``` - -### How QueryValueJson Is Serialized - -`Dictionary` values are JSON-serialized by `HttpRequestHandler`: - -``` -QueryValueJson = { "title": {"$ne": "Draft"}, "color": {"$in": ["red","blue"]} } -→ query={"title":{"$ne":"Draft"},"color":{"$in":["red","blue"]}} -``` - -This becomes a single `query=` URL parameter with the JSON as its value. - -### Sync API Patterns - -```csharp -// Initial sync (all published content) -SyncStack result = await client.Sync(new SyncStack() { Type = SyncType.entry_published }); - -// Paginated initial sync -SyncStack result = await client.SyncRecursive(parameters); -// result.PaginationToken — continue paginating if not null -// result.SyncToken — use for next delta sync - -// Delta sync (changes since last sync) -SyncStack delta = await client.SyncToken(result.SyncToken); - -// Manual pagination loop -SyncStack page = initialResult; -while (page.PaginationToken != null) -{ - page = await client.SyncPaginationToken(page.PaginationToken); - // process page.Items -} -``` - -### Taxonomy Query Patterns - -```csharp -// Create taxonomy query via client.Taxonomies() -Query taxonomyQuery = client.Taxonomies(); - -// Same filter methods apply -taxonomyQuery.Where("taxonomies.animals", new Dictionary { - { "$eq", "mammals" } -}); - -var results = await taxonomyQuery.Find(); -``` - -**URL paths** - -- Normal content-type query: `{stack.Config.BaseUrl}/content_types/{uid}/entries` -- Taxonomy query (`client.Taxonomies()`): `{stack.Config.BaseUrl}/taxonomies/entries` - -Filter and pagination APIs are the same; only the base path differs. - -### AssetLibrary Query Patterns - -```csharp -AssetLibrary assetLib = client.Assets(); -assetLib.Skip(0).Limit(100); -assetLib.IncludeCount(); -assetLib.SetLocale("en-us"); -ContentstackCollection assets = await assetLib.FetchAll(); -``` - -### Common Mistakes to Avoid - -- Never set `"environment"` key manually in `UrlQueries` — it is always injected from `Config.Environment` in `Exec()` -- Never call `ProcessRequest` directly — always go through model methods (`Find`, `Fetch`, etc.) -- Never modify `QueryValueJson` from outside `Query` — use the public fluent methods -- `And()` / `Or()` take full `Query` instances, not raw dictionaries -- `string[]` values in `UrlQueries` become repeated URL params, not JSON arrays — use for tags, not Mongo operators diff --git a/skills/sdk-core-patterns/SKILL.md b/skills/sdk-core-patterns/SKILL.md deleted file mode 100644 index b6e36e3a..00000000 --- a/skills/sdk-core-patterns/SKILL.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -name: sdk-core-patterns -description: Core architecture of the Contentstack .NET Delivery SDK — namespaces, ContentstackClient factory, Config/ContentstackOptions, HttpRequestHandler (HttpWebRequest GET), plugin hooks, ASP.NET Core DI, and multi-targeting. Use when working on new SDK features, adding model types, wiring DI, understanding request flow, or onboarding to the codebase. ---- - -# SDK Core Patterns - -## When to use - -- Onboarding, request flow, or where types live. -- Changing HTTP behavior, config, plugins, or DI. -- Anything that must go through `HttpRequestHandler`. - -## Namespace map - -| Namespace | Purpose | -|-----------|---------| -| `Contentstack.Core` | `ContentstackClient` — root entry point | -| `Contentstack.Core.Models` | `Query`, `Entry`, `Asset`, `AssetLibrary`, `ContentType`, `GlobalField`, `GlobalFieldQuery`, `Taxonomy`, `SyncStack`, `ContentstackCollection` | -| `Contentstack.Core.Configuration` | `ContentstackOptions`, `Config` (internal), `LivePreviewConfig` | -| `Contentstack.Core.Internals` | `HttpRequestHandler`, exceptions, enums, converters, constants — all `internal` | -| `Contentstack.Core.Interfaces` | `IContentstackPlugin` | -| `Contentstack.AspNetCore` | `IServiceCollectionExtensions` for DI registration | - -## Invariants - -- **`ContentstackClient`** is the only public entry point for creating stack operations. -- **All HTTP** goes through **`HttpRequestHandler.ProcessRequest`** — no `HttpClient`, no bypassing for feature work. -- **GET + query string** for requests; plugins run `OnRequest` / `OnResponse` around the call. -- **Multi-target:** `netstandard2.0`, `net47`, `net472` — no `System.Text.Json`; Newtonsoft.Json throughout. -- **Version:** `Directory.Build.props` only — do not set `` in individual `.csproj` files. - -## ContentstackClient (compact) - -```csharp -var client = new ContentstackClient(options); - -client.ContentType("uid").Query().Find(); -client.ContentType("uid").Entry("uid"); -client.Assets(); client.Asset("uid"); -client.GlobalField("uid"); client.GlobalFields(); -client.Taxonomies(); client.Sync(...); -``` - -## Options → Config → BaseUrl - -`ContentstackOptions` → internal `Config` → `BaseUrl` (region + host + version). Required: `ApiKey`, `DeliveryToken`, `Environment`. Region table, live preview host switching, and internal client state are in **SDK architecture reference** below. ASP.NET Core registration is covered under **ASP.NET Core integration** below. - - -## SDK Architecture Reference - -### Full Request Flow - -``` -ContentstackClient - └── ContentType("uid") → ContentType - └── Query() → Query - └── Find() → Query.Exec() - └── HttpRequestHandler.ProcessRequest(url, headers, bodyJson) - ├── Serialize BodyJson → query string - ├── Create HttpWebRequest (GET) - ├── Set headers (api_key, access_token, branch, x-user-agent) - ├── foreach plugin: OnRequest(client, request) - ├── await request.GetResponseAsync() - ├── foreach plugin: OnResponse(client, request, response, body) - └── return JSON string → parsed in Query.parseJObject -``` - -### Config.BaseUrl Composition - -``` -Protocol Region Code Host Version -"https://" "" "cdn.contentstack.io" "/v3" → US (default) -"https://" "eu-" "cdn.contentstack.com" "/v3" → EU -"https://" "azure-na-" "cdn.contentstack.com" "/v3" → AZURE_NA -"https://" "azure-eu-" "cdn.contentstack.com" "/v3" → AZURE_EU -"https://" "gcp-na-" "cdn.contentstack.com" "/v3" → GCP_NA -"https://" "au-" "cdn.contentstack.com" "/v3" → AU -``` - -`HostURL` defaults to `cdn.contentstack.io` for US, `cdn.contentstack.com` for all other regions. - -### Live Preview URL Resolution - -When `LivePreviewConfig.Enable == true` and `LivePreview != "init"` and `ContentTypeUID` matches the queried content type, `Config.getBaseUrl()` returns the live preview host instead of `BaseUrl`: - -``` -"https://{livePreviewConfig.Host}/{version}" -``` - -Additional headers injected: `live_preview`, `authorization` (management token) or `preview_token`, optional `release_id`, `preview_timestamp`. - -### Query String Serialization Rules (HttpRequestHandler) - -| Value type | Serialization | -|-----------|--------------| -| `string` | `key=value` | -| `string[]` | `key=v1&key=v2` (repeated) | -| `Dictionary` | `key={"$in":["a","b"]}` (JSON) | -| Other | `key=value.ToString()` | - -### ContentstackClient Internal State - -```csharp -internal string StackApiKey // from options -internal Dictionary _Headers // api_key, access_token/delivery_token -internal Dictionary _StackHeaders // shared across requests -internal LivePreviewConfig LivePreviewConfig // null if not configured -public List Plugins // empty by default -public JsonSerializerSettings SerializerSettings // for Fetch/Find -internal JsonSerializer Serializer // created from SerializerSettings -``` - -### How Models Get Stack Context - -All model constructors are `internal`. `ContentstackClient` methods set back-references: - -```csharp -// ContentType.cs internal wiring -internal ContentstackClient StackInstance { get; set; } - -// Query.cs -private ContentType ContentTypeInstance { get; set; } // for entry path -private ContentstackClient TaxonomyInstance { get; set; } // for taxonomy path -``` - -Models build their URL from `ContentTypeInstance.StackInstance.Config.BaseUrl` at call time (lazy). - -### Plugin Implementation Pattern - -```csharp -public class MyPlugin : IContentstackPlugin -{ - public Task OnRequest(ContentstackClient stack, HttpWebRequest request) - { - // Mutate request (add headers, log, etc.) - return Task.FromResult(request); - } - - public Task OnResponse(ContentstackClient stack, HttpWebRequest request, - HttpWebResponse response, string responseString) - { - // Inspect/transform response body string - return Task.FromResult(responseString); - } -} - -// Register -client.Plugins.Add(new MyPlugin()); -``` - -### ContentstackRegion Enum Values - -```csharp -public enum ContentstackRegion { US, EU, AZURE_NA, AZURE_EU, GCP_NA, AU } -``` - -`ContentstackRegionCode` (internal enum) maps to URL prefixes: `eu`, `azure_na`, `azure_eu`, `gcp_na`, `au`. Underscores are replaced with hyphens in the URL. - -### Key NuGet Dependencies (Contentstack.Core.csproj) - -| Package | Version | Purpose | -|---------|---------|---------| -| `Newtonsoft.Json` | 13.0.4 | All JSON serialization | -| `Microsoft.Extensions.Options` | 8.0.2 | `IOptions` | -| `Markdig` | 0.36.2 | Markdown processing in RTE fields | -| `contentstack.utils` | 1.0.6 | RTE embedded item resolution | - -### Solution Layout - -``` -Contentstack.Net.sln -├── Contentstack.Core/ ← Main SDK package (contentstack.csharp on NuGet) -├── Contentstack.AspNetCore/ ← DI extension (contentstack.aspnetcore on NuGet) -├── Contentstack.Core.Tests/ ← Integration tests (net7.0, hits live API) -└── Contentstack.Core.Unit.Tests/ ← Unit tests (no network) -``` - -Version shared via `Directory.Build.props` → `2.26.0` (or current). - -### Supporting internals (maintainers) - -When debugging HTTP, serialization edges, or multi-target behavior, these types in `Contentstack.Core/Internals/` are often involved. They are not public API. - -| Area | Types / files | -|------|----------------| -| Async helpers around `HttpWebRequest` | `WebRequestAsyncExtensions.cs` | -| Version string / user-agent composition | `VersionUtility.cs`, `StackOutput.cs` | -| JSON / value coercion helpers | `ContentstackConvert.cs` | -| Language / locale enums | `LanguageEnums.cs` | - -Prefer changing behavior through `HttpRequestHandler`, `Config`, and public models rather than exposing these types. - -## ASP.NET Core integration - -Source: [`Contentstack.AspNetCore/IServiceCollectionExtensions.cs`](../../../Contentstack.AspNetCore/IServiceCollectionExtensions.cs). - -### Registration - -Two overloads register the same services: - -```csharp -public static IServiceCollection AddContentstack(this IServiceCollection services, IConfigurationRoot configuration) -public static IServiceCollection AddContentstack(this IServiceCollection services, IConfiguration configuration) -``` - -Both: - -1. `services.AddOptions()` -2. `services.Configure(configuration.GetSection("ContentstackOptions"))` -3. `services.TryAddTransient()` - -### Configuration section - -Bind options from configuration using section name **`ContentstackOptions`**: - -```json -{ - "ContentstackOptions": { - "ApiKey": "...", - "DeliveryToken": "...", - "Environment": "..." - } -} -``` - -Adjust property names to match [`ContentstackOptions`](../../../Contentstack.Core/Configuration/ContentstackOptions.cs) public properties. - -### Service lifetime - -`ContentstackClient` is registered as **transient** (`TryAddTransient`). Each resolution gets a new instance; use this when injecting into short-lived scopes or when the app expects a fresh client per operation. - -### Usage in app code - -Inject `ContentstackClient` or `IOptions` as needed after calling `AddContentstack` in `Program.cs` / `Startup.cs`: - -```csharp -services.AddContentstack(configuration); -``` - -Ensure `configuration` includes the `ContentstackOptions` section (e.g. `appsettings.json`, environment variables, user secrets). diff --git a/skills/testing/SKILL.md b/skills/testing/SKILL.md deleted file mode 100644 index e76b7bce..00000000 --- a/skills/testing/SKILL.md +++ /dev/null @@ -1,334 +0,0 @@ ---- -name: testing -description: Testing patterns for the Contentstack .NET SDK — unit tests using AutoFixture and private field reflection (no network), and integration tests using IntegrationTestBase, TestDataHelper, LogArrange/LogAct/LogAssert, xUnit traits, and RequestLoggingPlugin. Use when writing new tests, adding coverage for a new feature, debugging integration test failures, or understanding the test structure. ---- - -# Testing - -## When to use - -- New unit tests for `Query` or models (reflection on `QueryValueJson` / `UrlQueries`). -- New or failing integration tests against the live API. - -## Projects - -| Project | Framework | Purpose | -|---------|-----------|---------| -| `Contentstack.Core.Unit.Tests` | xUnit + AutoFixture | No network; assert internal state via reflection | -| `Contentstack.Core.Tests` | xUnit, net7.0 | Live API; requires `app.config` (or equivalent) credentials | - -## Invariants - -- Unit tests: **no real network** — use AutoFixture for options, reflection for private dictionaries. -- Integration tests: **never commit secrets** — credentials from `app.config` / env locally. -- New `Query` methods: unit test operators + null/invalid cases; integration test when behavior is API-bound. - -## Testing Patterns Reference - -### Complete Unit Test Template - -```csharp -using System.Collections.Generic; -using System.Reflection; -using AutoFixture; -using Contentstack.Core; -using Contentstack.Core.Configuration; -using Contentstack.Core.Models; -using Contentstack.Core.Internals; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Contentstack.Core.Unit.Tests -{ - public class MyFeatureUnitTests - { - private readonly IFixture _fixture = new Fixture(); - private ContentstackClient _client; - - public MyFeatureUnitTests() - { - var options = new ContentstackOptions() - { - ApiKey = _fixture.Create(), - DeliveryToken = _fixture.Create(), - Environment = _fixture.Create() - }; - _client = new ContentstackClient(new OptionsWrapper(options)); - } - - private Query CreateQuery(string contentTypeId = "source") - => _client.ContentType(contentTypeId).Query(); - - // Helper: get private QueryValueJson - private Dictionary GetQueryValueJson(Query query) - { - var field = typeof(Query).GetField("QueryValueJson", - BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy); - return (Dictionary)field?.GetValue(query); - } - - // Helper: get private UrlQueries - private Dictionary GetUrlQueries(Query query) - { - var field = typeof(Query).GetField("UrlQueries", - BindingFlags.NonPublic | BindingFlags.Instance); - return (Dictionary)field?.GetValue(query); - } - - [Fact] - public void MyOperator_AddsCorrectQueryParameter() - { - var query = CreateQuery(); - var key = _fixture.Create(); - - var result = query.MyOperator(key, "value"); - - Assert.Equal(query, result); // fluent return - var qvj = GetQueryValueJson(query); - Assert.True(qvj.ContainsKey(key)); - var inner = qvj[key] as Dictionary; - Assert.True(inner.ContainsKey("$myop")); - Assert.Equal("value", inner["$myop"]); - } - - [Fact] - public void MyUrlParam_AddsToUrlQueries() - { - var query = CreateQuery(); - - query.SetLocale("en-us"); - - var urlQueries = GetUrlQueries(query); - Assert.Equal("en-us", urlQueries["locale"]); - } - - [Fact] - public void MyOperator_WithNullKey_ThrowsQueryFilterException() - { - var query = CreateQuery(); - Assert.Throws(() => query.MyOperator(null, "value")); - } - - [Fact] - public void MyOperator_WithEmptyKey_ThrowsQueryFilterException() - { - var query = CreateQuery(); - Assert.Throws(() => query.MyOperator(string.Empty, "value")); - } - - [Fact] - public void MyOperator_ReturnsQueryForChaining() - { - var query = CreateQuery(); - var result = query - .MyOperator("field1", "value1") - .MyOperator("field2", "value2"); - Assert.Equal(query, result); - } - } -} -``` - -### Complete Integration Test Template - -```csharp -using System.Threading.Tasks; -using Xunit; -using Xunit.Abstractions; -using Contentstack.Core.Models; -using Contentstack.Core.Tests.Helpers; - -namespace Contentstack.Core.Tests.Integration.MyFeatureTests -{ - public class MyFeatureComprehensiveTest : IntegrationTestBase - { - public MyFeatureComprehensiveTest(ITestOutputHelper output) : base(output) - { - } - - [Fact(DisplayName = "MyFeature - BasicOperation ReturnsExpectedResult")] - public async Task MyFeature_BasicOperation_ReturnsExpectedResult() - { - // Arrange - LogArrange("Setting up basic operation test"); - LogContext("ContentType", TestDataHelper.SimpleContentTypeUid); - - var client = CreateClient(); - var query = client.ContentType(TestDataHelper.SimpleContentTypeUid).Query(); - - // Act - LogAct("Executing query with my feature"); - query.MyOperator("uid", "someValue"); - var result = await query.Find(); - - // Assert - LogAssert("Verifying response structure"); - TestAssert.NotNull(result); - TestAssert.NotNull(result.Items); - TestAssert.True(result.Count >= 0, "Count should be non-negative"); - } - - [Fact(DisplayName = "MyFeature - WithInvalidParams ThrowsException")] - public async Task MyFeature_WithInvalidParams_ThrowsException() - { - // Arrange - LogArrange("Setting up error scenario"); - var client = CreateClient(); - - // Act & Assert - LogAct("Executing with invalid parameters"); - await Assert.ThrowsAsync(async () => - { - await client.ContentType("nonexistent_type_12345") - .Query().Find(); - }); - } - } -} -``` - -### Existing Unit Test Files — What Each Covers - -| File | Covers | -|------|--------| -| `QueryUnitTests.cs` | All `Query` filter/operator methods, UrlQueries params | -| `EntryUnitTests.cs` | `Entry` field access, URL construction, header setting | -| `AssetUnitTests.cs` | `Asset` model fields | -| `AssetLibraryUnitTests.cs` | `AssetLibrary` query params, Skip/Limit | -| `ContentstackClientUnitTests.cs` | Client initialization, header injection, factory methods | -| `ContentstackOptionsUnitTests.cs` | Options defaults, validation | -| `ContentstackExceptionUnitTests.cs` | Exception hierarchy, factory methods, message content | -| `ConfigUnitTests.cs` | BaseUrl composition, region codes | -| `ContentstackRegionUnitTests.cs` | Region enum → URL prefix mapping | -| `GlobalFieldUnitTests.cs` | GlobalField ID validation, URL construction | -| `GlobalFieldQueryUnitTests.cs` | GlobalFieldQuery filter methods | -| `TaxonomyUnitTests.cs` | Taxonomy query path | -| `JsonConverterUnitTests.cs` | CSJsonConverter attribute registration | -| `LivePreviewConfigUnitTests.cs` | LivePreviewConfig validation | - -### Existing Integration Test Folders — What Each Covers - -| Folder | Covers | -|--------|--------| -| `QueryTests/` | Query operators, complex filters, field queries, includes | -| `EntryTests/` | Entry fetch, field projection, references | -| `GlobalFieldsTests/` | Global field schemas, nested global fields | -| `SyncTests/` | Sync API, pagination tokens, delta sync | -| `AssetTests/` | Asset fetch, asset library queries | -| `ContentTypeTests/` | Content type fetch, schema queries | -| `LocalizationTests/` | Locale filtering, locale fallback chains | -| `PaginationTests/` | Skip/Limit behavior, count accuracy | -| `ErrorHandling/` | API error codes, exception types, invalid params | -| `LivePreview/` | Live preview URL routing, token headers | -| `ModularBlocksTests/` | Modular block field deserialization | -| `MetadataTests/` | Entry metadata fields | -| `TaxonomyTests/` | Taxonomy query path, taxonomy filtering | -| `VariantsTests/` | Entry variant headers, variant content | -| `BranchTests/` | Branch header injection | - -### MockHttpHandler Pattern (Unit Tests) - -When you need to mock HTTP responses without network: - -```csharp -// In Mokes/MockHttpHandler.cs — extend for new mock scenarios -// MockResponse.cs — add JSON fixture strings for new response shapes -// MockInfrastructureTest.cs — base class wiring MockHttpHandler into client -``` - -### TestAssert Wrappers - -Use `TestAssert.*` instead of raw `Assert.*` in integration tests — they log assertion context to `ITestOutputHelper`: - -```csharp -TestAssert.NotNull(result); -TestAssert.Equal(expected, actual); -TestAssert.True(condition, "failure message"); -TestAssert.False(condition, "failure message"); -TestAssert.IsAssignableFrom>(result.Items); -TestAssert.Matches("^blt[a-zA-Z0-9]+$", entry.Uid); -``` - -### app.config Keys for Integration Tests - -Integration tests read config from `Contentstack.Core.Tests/app.config`: - -```xml - - - - - - - - - - -``` - -Never commit real credentials. Use environment variables or a secrets manager in CI. - -### Running Tests - -```bash -# Unit tests only (no credentials needed) -dotnet test Contentstack.Core.Unit.Tests/ - -# Integration tests (requires app.config with valid credentials) -dotnet test Contentstack.Core.Tests/ - -# Run specific category -dotnet test --filter "Category=RetryIntegration" - -# Run specific test class -dotnet test --filter "FullyQualifiedName~QueryOperatorsComprehensiveTest" -``` - -### Mokes folder (unit tests) - -`Contentstack.Core.Unit.Tests/Mokes/`: - -- `MockHttpHandler.cs` — intercepts HTTP without network -- `MockResponse.cs` — sample JSON response fixtures -- `MockInfrastructureTest.cs` — base for tests needing mock HTTP -- `Utilities.cs` — test utility helpers - -### RequestLoggingPlugin (integration) - -`CreateClient()` on `IntegrationTestBase` adds `RequestLoggingPlugin`, which logs HTTP requests and responses via `ITestOutputHelper`. No extra setup required. - -Custom plugins for a test: - -```csharp -var client = CreateClient(); -client.Plugins.Add(new MyTestPlugin()); -``` - -### Test coverage guidelines - -- Unit test: every new public `Query` method (operator or URL param) -- Unit test: null/invalid input → expected exception type -- Integration test: happy path with real API response -- Integration test: verify response shape (`Items`, `Count`, fields) -- Place integration tests under the folder that matches the feature area - -### Integration test file conventions - -- Folders mirror features: `Integration/QueryTests/`, `Integration/EntryTests/`, `Integration/GlobalFieldsTests/`, etc. -- One test class per broad concern when it makes sense; file names often end in `Test.cs` / `Tests.cs` -- Use `LogArrange` / `LogAct` / `LogAssert` / `LogContext` from `IntegrationTestBase` (see templates above) - -#### xUnit traits (examples) - -```csharp -[Trait("Category", "RetryIntegration")] -[Trait("Category", "LivePreview")] -[Trait("Category", "Sync")] -``` - -#### DisplayName convention - -```csharp -[Fact(DisplayName = "Query Operations - Regex Complex Pattern Matches Correctly")] -// FeatureArea - ComponentAction Outcome -``` diff --git a/snyk.json b/snyk.json deleted file mode 100644 index 4093b1ad..00000000 --- a/snyk.json +++ /dev/null @@ -1,434 +0,0 @@ -[ - { - "vulnerabilities": [], - "ok": true, - "dependencyCount": 18, - "org": "contentstack-devex", - "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.\nversion: v1.25.1\nignore: {}\npatch: {}\n", - "isPrivate": true, - "licensesPolicy": { - "severities": {}, - "orgLicenseRules": { - "AGPL-1.0": { - "licenseType": "AGPL-1.0", - "severity": "high", - "instructions": "" - }, - "AGPL-3.0": { - "licenseType": "AGPL-3.0", - "severity": "high", - "instructions": "" - }, - "Artistic-1.0": { - "licenseType": "Artistic-1.0", - "severity": "medium", - "instructions": "" - }, - "Artistic-2.0": { - "licenseType": "Artistic-2.0", - "severity": "medium", - "instructions": "" - }, - "CDDL-1.0": { - "licenseType": "CDDL-1.0", - "severity": "medium", - "instructions": "" - }, - "CPOL-1.02": { - "licenseType": "CPOL-1.02", - "severity": "high", - "instructions": "" - }, - "EPL-1.0": { - "licenseType": "EPL-1.0", - "severity": "medium", - "instructions": "" - }, - "GPL-2.0": { - "licenseType": "GPL-2.0", - "severity": "high", - "instructions": "" - }, - "GPL-3.0": { - "licenseType": "GPL-3.0", - "severity": "high", - "instructions": "" - }, - "LGPL-2.0": { - "licenseType": "LGPL-2.0", - "severity": "medium", - "instructions": "" - }, - "LGPL-2.1": { - "licenseType": "LGPL-2.1", - "severity": "medium", - "instructions": "" - }, - "LGPL-3.0": { - "licenseType": "LGPL-3.0", - "severity": "medium", - "instructions": "" - }, - "MPL-1.1": { - "licenseType": "MPL-1.1", - "severity": "medium", - "instructions": "" - }, - "MPL-2.0": { - "licenseType": "MPL-2.0", - "severity": "medium", - "instructions": "" - }, - "MS-RL": { - "licenseType": "MS-RL", - "severity": "medium", - "instructions": "" - }, - "SimPL-2.0": { - "licenseType": "SimPL-2.0", - "severity": "high", - "instructions": "" - } - } - }, - "packageManager": "nuget", - "ignoreSettings": { - "adminOnly": false, - "reasonRequired": false, - "disregardFilesystemIgnores": false - }, - "summary": "No known vulnerabilities", - "filesystemPolicy": false, - "uniqueCount": 0, - "targetFile": "Contentstack.AspNetCore/obj/project.assets.json", - "projectName": "contentstack-dotnet", - "foundProjectCount": 4, - "displayTargetFile": "Contentstack.AspNetCore/obj/project.assets.json", - "hasUnknownVersions": false, - "path": "/Users/om.pawar/Desktop/SDKs/contentstack-dotnet" - }, - { - "vulnerabilities": [], - "ok": true, - "dependencyCount": 103, - "org": "contentstack-devex", - "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.\nversion: v1.25.1\nignore: {}\npatch: {}\n", - "isPrivate": true, - "licensesPolicy": { - "severities": {}, - "orgLicenseRules": { - "AGPL-1.0": { - "licenseType": "AGPL-1.0", - "severity": "high", - "instructions": "" - }, - "AGPL-3.0": { - "licenseType": "AGPL-3.0", - "severity": "high", - "instructions": "" - }, - "Artistic-1.0": { - "licenseType": "Artistic-1.0", - "severity": "medium", - "instructions": "" - }, - "Artistic-2.0": { - "licenseType": "Artistic-2.0", - "severity": "medium", - "instructions": "" - }, - "CDDL-1.0": { - "licenseType": "CDDL-1.0", - "severity": "medium", - "instructions": "" - }, - "CPOL-1.02": { - "licenseType": "CPOL-1.02", - "severity": "high", - "instructions": "" - }, - "EPL-1.0": { - "licenseType": "EPL-1.0", - "severity": "medium", - "instructions": "" - }, - "GPL-2.0": { - "licenseType": "GPL-2.0", - "severity": "high", - "instructions": "" - }, - "GPL-3.0": { - "licenseType": "GPL-3.0", - "severity": "high", - "instructions": "" - }, - "LGPL-2.0": { - "licenseType": "LGPL-2.0", - "severity": "medium", - "instructions": "" - }, - "LGPL-2.1": { - "licenseType": "LGPL-2.1", - "severity": "medium", - "instructions": "" - }, - "LGPL-3.0": { - "licenseType": "LGPL-3.0", - "severity": "medium", - "instructions": "" - }, - "MPL-1.1": { - "licenseType": "MPL-1.1", - "severity": "medium", - "instructions": "" - }, - "MPL-2.0": { - "licenseType": "MPL-2.0", - "severity": "medium", - "instructions": "" - }, - "MS-RL": { - "licenseType": "MS-RL", - "severity": "medium", - "instructions": "" - }, - "SimPL-2.0": { - "licenseType": "SimPL-2.0", - "severity": "high", - "instructions": "" - } - } - }, - "packageManager": "nuget", - "ignoreSettings": { - "adminOnly": false, - "reasonRequired": false, - "disregardFilesystemIgnores": false - }, - "summary": "No known vulnerabilities", - "filesystemPolicy": false, - "uniqueCount": 0, - "targetFile": "Contentstack.Core.Tests/obj/project.assets.json", - "projectName": "contentstack-dotnet", - "foundProjectCount": 4, - "displayTargetFile": "Contentstack.Core.Tests/obj/project.assets.json", - "hasUnknownVersions": false, - "path": "/Users/om.pawar/Desktop/SDKs/contentstack-dotnet" - }, - { - "vulnerabilities": [], - "ok": true, - "dependencyCount": 103, - "org": "contentstack-devex", - "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.\nversion: v1.25.1\nignore: {}\npatch: {}\n", - "isPrivate": true, - "licensesPolicy": { - "severities": {}, - "orgLicenseRules": { - "AGPL-1.0": { - "licenseType": "AGPL-1.0", - "severity": "high", - "instructions": "" - }, - "AGPL-3.0": { - "licenseType": "AGPL-3.0", - "severity": "high", - "instructions": "" - }, - "Artistic-1.0": { - "licenseType": "Artistic-1.0", - "severity": "medium", - "instructions": "" - }, - "Artistic-2.0": { - "licenseType": "Artistic-2.0", - "severity": "medium", - "instructions": "" - }, - "CDDL-1.0": { - "licenseType": "CDDL-1.0", - "severity": "medium", - "instructions": "" - }, - "CPOL-1.02": { - "licenseType": "CPOL-1.02", - "severity": "high", - "instructions": "" - }, - "EPL-1.0": { - "licenseType": "EPL-1.0", - "severity": "medium", - "instructions": "" - }, - "GPL-2.0": { - "licenseType": "GPL-2.0", - "severity": "high", - "instructions": "" - }, - "GPL-3.0": { - "licenseType": "GPL-3.0", - "severity": "high", - "instructions": "" - }, - "LGPL-2.0": { - "licenseType": "LGPL-2.0", - "severity": "medium", - "instructions": "" - }, - "LGPL-2.1": { - "licenseType": "LGPL-2.1", - "severity": "medium", - "instructions": "" - }, - "LGPL-3.0": { - "licenseType": "LGPL-3.0", - "severity": "medium", - "instructions": "" - }, - "MPL-1.1": { - "licenseType": "MPL-1.1", - "severity": "medium", - "instructions": "" - }, - "MPL-2.0": { - "licenseType": "MPL-2.0", - "severity": "medium", - "instructions": "" - }, - "MS-RL": { - "licenseType": "MS-RL", - "severity": "medium", - "instructions": "" - }, - "SimPL-2.0": { - "licenseType": "SimPL-2.0", - "severity": "high", - "instructions": "" - } - } - }, - "packageManager": "nuget", - "ignoreSettings": { - "adminOnly": false, - "reasonRequired": false, - "disregardFilesystemIgnores": false - }, - "summary": "No known vulnerabilities", - "filesystemPolicy": false, - "uniqueCount": 0, - "targetFile": "Contentstack.Core.Unit.Tests/obj/project.assets.json", - "projectName": "contentstack-dotnet", - "foundProjectCount": 4, - "displayTargetFile": "Contentstack.Core.Unit.Tests/obj/project.assets.json", - "hasUnknownVersions": false, - "path": "/Users/om.pawar/Desktop/SDKs/contentstack-dotnet" - }, - { - "vulnerabilities": [], - "ok": true, - "dependencyCount": 14, - "org": "contentstack-devex", - "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.\nversion: v1.25.1\nignore: {}\npatch: {}\n", - "isPrivate": true, - "licensesPolicy": { - "severities": {}, - "orgLicenseRules": { - "AGPL-1.0": { - "licenseType": "AGPL-1.0", - "severity": "high", - "instructions": "" - }, - "AGPL-3.0": { - "licenseType": "AGPL-3.0", - "severity": "high", - "instructions": "" - }, - "Artistic-1.0": { - "licenseType": "Artistic-1.0", - "severity": "medium", - "instructions": "" - }, - "Artistic-2.0": { - "licenseType": "Artistic-2.0", - "severity": "medium", - "instructions": "" - }, - "CDDL-1.0": { - "licenseType": "CDDL-1.0", - "severity": "medium", - "instructions": "" - }, - "CPOL-1.02": { - "licenseType": "CPOL-1.02", - "severity": "high", - "instructions": "" - }, - "EPL-1.0": { - "licenseType": "EPL-1.0", - "severity": "medium", - "instructions": "" - }, - "GPL-2.0": { - "licenseType": "GPL-2.0", - "severity": "high", - "instructions": "" - }, - "GPL-3.0": { - "licenseType": "GPL-3.0", - "severity": "high", - "instructions": "" - }, - "LGPL-2.0": { - "licenseType": "LGPL-2.0", - "severity": "medium", - "instructions": "" - }, - "LGPL-2.1": { - "licenseType": "LGPL-2.1", - "severity": "medium", - "instructions": "" - }, - "LGPL-3.0": { - "licenseType": "LGPL-3.0", - "severity": "medium", - "instructions": "" - }, - "MPL-1.1": { - "licenseType": "MPL-1.1", - "severity": "medium", - "instructions": "" - }, - "MPL-2.0": { - "licenseType": "MPL-2.0", - "severity": "medium", - "instructions": "" - }, - "MS-RL": { - "licenseType": "MS-RL", - "severity": "medium", - "instructions": "" - }, - "SimPL-2.0": { - "licenseType": "SimPL-2.0", - "severity": "high", - "instructions": "" - } - } - }, - "packageManager": "nuget", - "ignoreSettings": { - "adminOnly": false, - "reasonRequired": false, - "disregardFilesystemIgnores": false - }, - "summary": "No known vulnerabilities", - "filesystemPolicy": false, - "uniqueCount": 0, - "targetFile": "Contentstack.Core/obj/project.assets.json", - "projectName": "contentstack-dotnet", - "foundProjectCount": 4, - "displayTargetFile": "Contentstack.Core/obj/project.assets.json", - "hasUnknownVersions": false, - "path": "/Users/om.pawar/Desktop/SDKs/contentstack-dotnet" - } -]