Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/sca-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ jobs:
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --file=Contentstack.Management.Core/obj/project.assets.json --fail-on=all
json: true
args: --file=Contentstack.Management.Core/obj/project.assets.json --fail-on=all --json-file-output=snyk.json
continue-on-error: true
- uses: contentstack/sca-policy@main
170 changes: 170 additions & 0 deletions Contentstack.Management.Core.Tests/Helpers/PersonalizeTestHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Contentstack.Management.Core.Endpoints;

namespace Contentstack.Management.Core.Tests.Helpers
{
/// <summary>
/// Raw-HTTP client for the Contentstack Personalize Management API, used only by
/// integration tests. The core SDK has no Project/Audience/Experience wrapper classes
/// (Personalize is a separate product surface from Content Management), so this helper
/// drives Personalize directly to auto-provision real Variant Group/Variant data on a
/// stack instead of relying on a hardcoded variant UID.
///
/// NOTE(verify at implementation time): exact endpoint paths, payload shapes, and
/// required headers below are best-effort based on Contentstack's published Personalize
/// API conventions and have not been exercised against a live org in this change.
/// Confirm against https://www.contentstack.com/docs/developers/apis/personalize-management-api
/// before relying on this in CI, and adjust the marked TODO(verify) spots as needed.
/// </summary>
internal class PersonalizeTestHelper
{
private readonly HttpClient _httpClient;
private readonly string _baseUrl;
private readonly string _organizationUid;

internal PersonalizeTestHelper(string authtoken, string organizationUid, string region = "na")
{
if (string.IsNullOrEmpty(authtoken))
{
throw new ArgumentException("authtoken is required", nameof(authtoken));
}
if (string.IsNullOrEmpty(organizationUid))
{
throw new ArgumentException("organizationUid is required", nameof(organizationUid));
}

_organizationUid = organizationUid;
_baseUrl = Endpoint.GetContentstackEndpoint(region, "personalizeManagement").TrimEnd('/');

_httpClient = new HttpClient(new LoggingHttpHandler())
{
Timeout = TimeSpan.FromSeconds(30)
};
_httpClient.DefaultRequestHeaders.Add("authtoken", authtoken);
_httpClient.DefaultRequestHeaders.Add("organization_uid", organizationUid);
}

/// <summary>
/// Creates a Personalize project connected to the given stack, so that Audiences/
/// Experiences created within it can auto-provision Variant Groups on that stack.
/// TODO(verify): confirm path is "/projects" and payload shape against Personalize docs.
/// </summary>
internal async Task<string> CreateProjectAsync(string stackApiKey, string projectName)
{
var payload = new JsonObject
{
["name"] = projectName,
["connectedStackApiKey"] = stackApiKey
};

var project = await PostAsync("/projects", payload, null);
return project?["uid"]?.ToString();
}

/// <summary>
/// Returns the uid of an existing audience on the project if one is found, otherwise
/// creates a new default audience and returns its uid.
/// TODO(verify): confirm paths "/audiences" (GET list / POST create) and payload shape.
/// </summary>
internal async Task<string> GetOrCreateDefaultAudienceAsync(string projectUid, string audienceName)
{
var existing = await GetAsync("/audiences", projectUid);
var audiences = existing?["audiences"]?.AsArray();
if (audiences != null && audiences.Count > 0)
{
return audiences[0]?["uid"]?.ToString();
}

var payload = new JsonObject
{
["name"] = audienceName,
["definition"] = new JsonObject
{
["rules"] = new JsonArray(),
["operator"] = "and"
}
};

var audience = await PostAsync("/audiences", payload, projectUid);
return audience?["uid"]?.ToString();
}

/// <summary>
/// Creates an Experience linked to the given audience. In the real product, creating
/// an Experience on a stack-connected project auto-provisions a Variant Group (and its
/// Variants) on the Content Management side of that stack.
/// TODO(verify): confirm path "/experiences" and payload shape (variant count/short_uids).
/// </summary>
internal async Task<string> CreateExperienceAsync(string projectUid, string audienceUid, string experienceName)
{
var payload = new JsonObject
{
["name"] = experienceName,
["audiences"] = new JsonArray(audienceUid),
["variants"] = new JsonArray(new JsonObject { ["name"] = "Variant A" })
};

var experience = await PostAsync("/experiences", payload, projectUid);
return experience?["uid"]?.ToString();
}

/// <summary>Best-effort teardown of an experience created for a test run. Failures are non-fatal.</summary>
internal async Task DeleteExperienceAsync(string projectUid, string experienceUid)
{
await DeleteAsync($"/experiences/{experienceUid}", projectUid);
}

/// <summary>Best-effort teardown of a project created for a test run. Failures are non-fatal.</summary>
internal async Task DeleteProjectAsync(string projectUid)
{
await DeleteAsync($"/projects/{projectUid}", null);
}

private async Task<JsonObject> PostAsync(string path, JsonObject payload, string projectUid)
{
using var request = new HttpRequestMessage(HttpMethod.Post, _baseUrl + path);
ApplyProjectHeader(request, projectUid);
request.Content = new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json");

using var response = await _httpClient.SendAsync(request);
string body = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();

return string.IsNullOrEmpty(body) ? null : JsonNode.Parse(body)?.AsObject();
}

private async Task<JsonObject> GetAsync(string path, string projectUid)
{
using var request = new HttpRequestMessage(HttpMethod.Get, _baseUrl + path);
ApplyProjectHeader(request, projectUid);

using var response = await _httpClient.SendAsync(request);
string body = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();

return string.IsNullOrEmpty(body) ? null : JsonNode.Parse(body)?.AsObject();
}

private async Task DeleteAsync(string path, string projectUid)
{
using var request = new HttpRequestMessage(HttpMethod.Delete, _baseUrl + path);
ApplyProjectHeader(request, projectUid);
using var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
}

private static void ApplyProjectHeader(HttpRequestMessage request, string projectUid)
{
if (!string.IsNullOrEmpty(projectUid))
{
request.Headers.TryAddWithoutValidation("x-project-uid", projectUid);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Contentstack.Management.Core;
using Contentstack.Management.Core.Exceptions;
using Contentstack.Management.Core.Models;
using Contentstack.Management.Core.Models.Token;
using Contentstack.Management.Core.Tests.Helpers;
using Contentstack.Management.Core.Tests.Model;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand Down Expand Up @@ -1191,5 +1192,71 @@ private static void AssertStackAuthError(Exception ex, string assertionName)
}

#endregion

#region Global management token (shared infrastructure for variant/personalize tests)

[TestMethod]
[DoNotParallelize]
public void Test065_Should_Create_Management_Token()
{
TestOutputLogger.LogContext("TestScenario", "CreateGlobalManagementToken");
AssertStackApiKeyOrInconclusive();
try
{
Stack stack = _client.Stack(Contentstack.Stack.APIKey);
var model = new ManagementTokenModel
{
Name = "DotNet SDK Integration Test Token",
Description = "Shared management token used by the .NET SDK integration test suite",
Scope = new List<TokenScope>
{
new TokenScope
{
Module = "content_type",
ACL = new Dictionary<string, string> { { "read", "true" }, { "write", "true" } }
},
new TokenScope
{
Module = "entry",
ACL = new Dictionary<string, string> { { "read", "true" }, { "write", "true" } }
},
new TokenScope
{
Module = "asset",
ACL = new Dictionary<string, string> { { "read", "true" }, { "write", "true" } }
},
new TokenScope
{
Module = "environment",
ACL = new Dictionary<string, string> { { "read", "true" }, { "write", "true" } }
},
new TokenScope
{
Module = "taxonomy",
ACL = new Dictionary<string, string> { { "read", "true" }, { "write", "true" } }
}
}
};

ContentstackResponse contentstackResponse = stack.ManagementTokens().Create(model);

AssertLogger.IsTrue(contentstackResponse.IsSuccessStatusCode, $"Create management token failed: {contentstackResponse.OpenResponse()}", "CreateManagementTokenSuccess");

File.WriteAllText("./managementTokenInfo.txt", contentstackResponse.OpenResponse());

ManagementTokenResponse tokenResponse = contentstackResponse.OpenTResponse<ManagementTokenResponse>();
AssertLogger.IsNotNull(tokenResponse.Token, "tokenResponse.Token");
AssertLogger.IsNotNull(tokenResponse.Token.Uid, "tokenResponse.Token.Uid");
AssertLogger.IsNotNull(tokenResponse.Token.Token, "tokenResponse.Token.Token");

TestOutputLogger.LogContext("ManagementTokenUid", tokenResponse.Token.Uid ?? "");
}
catch (Exception e)
{
AssertLogger.Fail(e.Message);
}
}

#endregion
}
}
Loading
Loading