Skip to content

Commit f6f9843

Browse files
authored
Merge pull request #195 from contentstack/enhc/DX-7278
feat: add multi-region endpoint resolution via Endpoint static class
2 parents bdd5f0e + 816b480 commit f6f9843

6 files changed

Lines changed: 716 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
- All modules fully migrated to System.Text.Json: AuditLog, Branch, BulkOperation, ContentType, DeliveryToken, Entry, EntryVariant, Environment, Extension, GlobalField, Label, Locale, ManagementToken, Organization, Release, Role, Stack, Taxonomy, Term, User, VariantGroup, Webhook, and Workflow
1616
- OAuth auto token refresh wired into the request pipeline
1717
- Upgraded target framework to .NET 10
18+
- **New:** Multi-region endpoint resolution via `Endpoint.GetContentstackEndpoint(region, service)` — resolves Contentstack service URLs for all 7 supported regions (NA, EU, AU, Azure-NA, Azure-EU, GCP-NA, GCP-EU) and 18 service keys (contentManagement, contentDelivery, auth, graphqlDelivery, preview, images, assets, automate, launch, developerHub, brandKit, genAI, personalizeManagement, personalizeEdge, composableStudio, assetManagement, and more).
19+
- **New:** `omitHttps` flag strips the `https://` scheme from returned URLs — pass directly to `ContentstackClientOptions.Host` (e.g. `new ContentstackClientOptions { Host = Endpoint.GetContentstackEndpoint("eu", "contentManagement", omitHttps: true) }`).
20+
- **New:** Case-insensitive region alias support — `"us"`, `"NA"`, `"AWS-NA"`, `"azure_na"` all resolve correctly to the same region.
21+
- **New:** `regions.json` registry auto-downloaded from `artifacts.contentstack.com` on first use and cached on disk — no setup required. The SDK self-heals if the file is missing.
22+
- **New:** `Scripts/refresh-region.cs` bundled inside the NuGet package — automatically placed in your project's `Scripts/` folder on first `dotnet build`. Run `dotnet run Scripts/refresh-region.cs` anytime to pull the latest regions from CDN.
1823

1924
## [v0.10.0](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.10.0)
2025
- Feat
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using Contentstack.Management.Core.Endpoints;
5+
using Microsoft.VisualStudio.TestTools.UnitTesting;
6+
7+
namespace Contentstack.Management.Core.Unit.Tests.Endpoints
8+
{
9+
[TestClass]
10+
public class EndpointTest
11+
{
12+
[TestInitialize]
13+
public void Setup() => Endpoint.ResetCache();
14+
15+
[TestCleanup]
16+
public void Teardown() => Endpoint.ResetCache();
17+
18+
// ------------------------------------------------------------------
19+
// Basic resolution
20+
// ------------------------------------------------------------------
21+
22+
[TestMethod]
23+
public void GetContentstackEndpoint_Na_ReturnsCorrectManagementUrl()
24+
{
25+
string url = Endpoint.GetContentstackEndpoint("na", "contentManagement");
26+
Assert.AreEqual("https://api.contentstack.io", url);
27+
}
28+
29+
[DataTestMethod]
30+
[DataRow("na")]
31+
[DataRow("eu")]
32+
[DataRow("au")]
33+
[DataRow("azure-na")]
34+
[DataRow("azure-eu")]
35+
[DataRow("gcp-na")]
36+
[DataRow("gcp-eu")]
37+
public void GetContentstackEndpoint_AllRegionIds_Resolve(string regionId)
38+
{
39+
string url = Endpoint.GetContentstackEndpoint(regionId, "contentManagement");
40+
Assert.IsFalse(string.IsNullOrEmpty(url));
41+
Assert.IsTrue(url.StartsWith("https://"));
42+
}
43+
44+
// ------------------------------------------------------------------
45+
// Alias resolution (case-insensitive, dash/underscore variants)
46+
// ------------------------------------------------------------------
47+
48+
[DataTestMethod]
49+
[DataRow("na")]
50+
[DataRow("us")]
51+
[DataRow("NA")]
52+
[DataRow("US")]
53+
[DataRow("AWS-NA")]
54+
[DataRow("aws_na")]
55+
[DataRow("AWS_NA")]
56+
public void GetContentstackEndpoint_NaAliasVariants_AllResolveToSameUrl(string alias)
57+
{
58+
string url = Endpoint.GetContentstackEndpoint(alias, "contentManagement");
59+
Assert.AreEqual("https://api.contentstack.io", url);
60+
}
61+
62+
[DataTestMethod]
63+
[DataRow("azure-na")]
64+
[DataRow("azure_na")]
65+
[DataRow("AZURE-NA")]
66+
[DataRow("AZURE_NA")]
67+
public void GetContentstackEndpoint_AzureNaAliasVariants_AllResolveToSameUrl(string alias)
68+
{
69+
string expected = Endpoint.GetContentstackEndpoint("azure-na", "contentManagement");
70+
string result = Endpoint.GetContentstackEndpoint(alias, "contentManagement");
71+
Assert.AreEqual(expected, result);
72+
}
73+
74+
[DataTestMethod]
75+
[DataRow("eu")]
76+
[DataRow("EU")]
77+
[DataRow("aws-eu")]
78+
[DataRow("AWS-EU")]
79+
[DataRow("aws_eu")]
80+
public void GetContentstackEndpoint_EuAliasVariants_AllResolveToSameUrl(string alias)
81+
{
82+
string expected = Endpoint.GetContentstackEndpoint("eu", "contentManagement");
83+
string result = Endpoint.GetContentstackEndpoint(alias, "contentManagement");
84+
Assert.AreEqual(expected, result);
85+
}
86+
87+
// ------------------------------------------------------------------
88+
// omitHttps flag
89+
// ------------------------------------------------------------------
90+
91+
[TestMethod]
92+
public void GetContentstackEndpoint_OmitHttps_StripsScheme()
93+
{
94+
string url = Endpoint.GetContentstackEndpoint("na", "contentManagement", omitHttps: true);
95+
Assert.IsFalse(url.StartsWith("https://"), "URL should not start with https://");
96+
Assert.IsFalse(url.StartsWith("http://"), "URL should not start with http://");
97+
Assert.IsTrue(url.Contains("."), "URL should contain a hostname");
98+
}
99+
100+
[TestMethod]
101+
public void GetContentstackEndpoint_OmitHttpsFalse_PreservesScheme()
102+
{
103+
string url = Endpoint.GetContentstackEndpoint("na", "contentManagement", omitHttps: false);
104+
Assert.IsTrue(url.StartsWith("https://"));
105+
}
106+
107+
[DataTestMethod]
108+
[DataRow("na")]
109+
[DataRow("eu")]
110+
[DataRow("au")]
111+
[DataRow("azure-na")]
112+
[DataRow("gcp-na")]
113+
public void GetContentstackEndpoint_OmitHttps_AllRegions_StripsScheme(string region)
114+
{
115+
string url = Endpoint.GetContentstackEndpoint(region, "contentManagement", omitHttps: true);
116+
Assert.IsFalse(url.StartsWith("https://"));
117+
Assert.IsFalse(url.StartsWith("http://"));
118+
}
119+
120+
// ------------------------------------------------------------------
121+
// Dictionary overload
122+
// ------------------------------------------------------------------
123+
124+
[TestMethod]
125+
public void GetContentstackEndpoint_DictOverload_ContainsManagementKey()
126+
{
127+
var dict = Endpoint.GetContentstackEndpoint("na");
128+
Assert.IsTrue(dict.ContainsKey("contentManagement"));
129+
Assert.AreEqual("https://api.contentstack.io", dict["contentManagement"]);
130+
}
131+
132+
[TestMethod]
133+
public void GetContentstackEndpoint_DictOverload_ContainsDeliveryKey()
134+
{
135+
var dict = Endpoint.GetContentstackEndpoint("na");
136+
Assert.IsTrue(dict.ContainsKey("contentDelivery"));
137+
}
138+
139+
[TestMethod]
140+
public void GetContentstackEndpoint_DictOverload_ReturnsMultipleServices()
141+
{
142+
var dict = Endpoint.GetContentstackEndpoint("na");
143+
Assert.IsTrue(dict.Count >= 2, "Should contain at least 2 service endpoints");
144+
}
145+
146+
[TestMethod]
147+
public void GetContentstackEndpoint_DictOverload_OmitHttps_StripsAllSchemes()
148+
{
149+
var dict = Endpoint.GetContentstackEndpoint("na", omitHttps: true);
150+
foreach (var kvp in dict)
151+
{
152+
Assert.IsFalse(kvp.Value.StartsWith("https://"),
153+
$"Service '{kvp.Key}' URL still has https:// prefix");
154+
}
155+
}
156+
157+
[DataTestMethod]
158+
[DataRow("na")]
159+
[DataRow("eu")]
160+
[DataRow("au")]
161+
[DataRow("azure-na")]
162+
[DataRow("azure-eu")]
163+
[DataRow("gcp-na")]
164+
[DataRow("gcp-eu")]
165+
public void GetContentstackEndpoint_DictOverload_AllRegions_ReturnNonEmpty(string region)
166+
{
167+
var dict = Endpoint.GetContentstackEndpoint(region);
168+
Assert.IsTrue(dict.Count > 0, $"Expected at least one endpoint for region '{region}'");
169+
}
170+
171+
// ------------------------------------------------------------------
172+
// Error cases
173+
// ------------------------------------------------------------------
174+
175+
[TestMethod]
176+
[ExpectedException(typeof(ArgumentException))]
177+
public void GetContentstackEndpoint_EmptyRegion_ThrowsArgumentException()
178+
{
179+
Endpoint.GetContentstackEndpoint("", "contentManagement");
180+
}
181+
182+
[TestMethod]
183+
[ExpectedException(typeof(ArgumentException))]
184+
public void GetContentstackEndpoint_WhitespaceRegion_ThrowsArgumentException()
185+
{
186+
Endpoint.GetContentstackEndpoint(" ", "contentManagement");
187+
}
188+
189+
[TestMethod]
190+
[ExpectedException(typeof(ArgumentException))]
191+
public void GetContentstackEndpoint_DictOverload_EmptyRegion_ThrowsArgumentException()
192+
{
193+
Endpoint.GetContentstackEndpoint("");
194+
}
195+
196+
[TestMethod]
197+
[ExpectedException(typeof(KeyNotFoundException))]
198+
public void GetContentstackEndpoint_UnknownRegion_ThrowsKeyNotFoundException()
199+
{
200+
Endpoint.GetContentstackEndpoint("xyz", "contentManagement");
201+
}
202+
203+
[TestMethod]
204+
[ExpectedException(typeof(KeyNotFoundException))]
205+
public void GetContentstackEndpoint_DictOverload_UnknownRegion_ThrowsKeyNotFoundException()
206+
{
207+
Endpoint.GetContentstackEndpoint("xyz");
208+
}
209+
210+
[TestMethod]
211+
[ExpectedException(typeof(KeyNotFoundException))]
212+
public void GetContentstackEndpoint_UnknownService_ThrowsKeyNotFoundException()
213+
{
214+
Endpoint.GetContentstackEndpoint("na", "unknownService");
215+
}
216+
217+
[TestMethod]
218+
public void GetContentstackEndpoint_UnknownRegion_ErrorMessageContainsInput()
219+
{
220+
try
221+
{
222+
Endpoint.GetContentstackEndpoint("badregion", "contentManagement");
223+
Assert.Fail("Expected KeyNotFoundException");
224+
}
225+
catch (KeyNotFoundException ex)
226+
{
227+
Assert.IsTrue(ex.Message.Contains("badregion"));
228+
}
229+
}
230+
231+
[TestMethod]
232+
public void GetContentstackEndpoint_UnknownService_ErrorMessageContainsServiceName()
233+
{
234+
try
235+
{
236+
Endpoint.GetContentstackEndpoint("na", "badService");
237+
Assert.Fail("Expected KeyNotFoundException");
238+
}
239+
catch (KeyNotFoundException ex)
240+
{
241+
Assert.IsTrue(ex.Message.Contains("badService"));
242+
}
243+
}
244+
245+
// ------------------------------------------------------------------
246+
// Cache behaviour
247+
// ------------------------------------------------------------------
248+
249+
[TestMethod]
250+
public void ResetCache_AllowsSubsequentCallToSucceed()
251+
{
252+
// First call populates cache
253+
string url1 = Endpoint.GetContentstackEndpoint("na", "contentManagement");
254+
255+
// Reset and call again — should reload from disk/CDN and return same value
256+
Endpoint.ResetCache();
257+
string url2 = Endpoint.GetContentstackEndpoint("na", "contentManagement");
258+
259+
Assert.AreEqual(url1, url2);
260+
}
261+
262+
[TestMethod]
263+
public void GetContentstackEndpoint_CalledTwice_ReturnsSameResult()
264+
{
265+
string url1 = Endpoint.GetContentstackEndpoint("eu", "contentManagement");
266+
string url2 = Endpoint.GetContentstackEndpoint("eu", "contentManagement");
267+
Assert.AreEqual(url1, url2);
268+
}
269+
270+
// ------------------------------------------------------------------
271+
// File path helper
272+
// ------------------------------------------------------------------
273+
274+
[TestMethod]
275+
public void GetLocalFilePath_EndsWithExpectedSegments()
276+
{
277+
string path = Endpoint.GetLocalFilePath();
278+
Assert.IsTrue(path.EndsWith(Path.Combine("Assets", "regions.json")),
279+
$"Expected path to end with Assets/regions.json, got: {path}");
280+
}
281+
282+
// ------------------------------------------------------------------
283+
// All 7 regions × contentManagement spot-checks
284+
// ------------------------------------------------------------------
285+
286+
[DataTestMethod]
287+
[DataRow("na", "https://api.contentstack.io")]
288+
[DataRow("us", "https://api.contentstack.io")]
289+
[DataRow("eu", "https://eu-api.contentstack.com")]
290+
[DataRow("au", "https://au-api.contentstack.com")]
291+
public void GetContentstackEndpoint_KnownRegions_ContentManagement_MatchExpected(
292+
string region, string expected)
293+
{
294+
string url = Endpoint.GetContentstackEndpoint(region, "contentManagement");
295+
Assert.AreEqual(expected, url);
296+
}
297+
298+
// ------------------------------------------------------------------
299+
// ID takes priority over alias (two-pass lookup)
300+
// ------------------------------------------------------------------
301+
302+
[TestMethod]
303+
public void GetContentstackEndpoint_IdTakesPriorityOverAlias()
304+
{
305+
// "eu" is both a valid region ID and an alias in some registries.
306+
// The two-pass lookup must return the ID match first.
307+
string byId = Endpoint.GetContentstackEndpoint("eu", "contentManagement");
308+
Assert.IsFalse(string.IsNullOrEmpty(byId));
309+
}
310+
}
311+
}

0 commit comments

Comments
 (0)