contentstack_utils 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: code-review
3
+ description: Use when reviewing or preparing a PR for this gem—behavior, tests, API compatibility, and release notes.
4
+ ---
5
+
6
+ # Code review – Contentstack Utils Ruby
7
+
8
+ ## When to use
9
+
10
+ - Authoring a pull request that changes rendering, models, or dependencies
11
+ - Reviewing someone else’s PR before merge
12
+ - Checking release readiness (version + changelog)
13
+
14
+ ## Instructions
15
+
16
+ ### Blockers (fix before merge)
17
+
18
+ - **Tests:** `bundle exec rake` (or `bundle exec rspec`) passes locally
19
+ - **Breaking API:** unintended removal or signature change of public methods on **`ContentstackUtils`**, **`ContentstackUtils::GQL`**, or **`ContentstackUtils::Model::Options`** without version bump and changelog entry
20
+ - **Security:** no secrets in code, fixtures, or logs; HTML output changes reviewed for injection or XSS risk when embedding untrusted fields
21
+
22
+ ### Major (should address)
23
+
24
+ - **CHANGELOG.md** updated for user-visible behavior fixes or features
25
+ - **`lib/contentstack_utils/version.rb`** updated when publishing a release (if this PR is release-bound)
26
+ - New JSON/RTE node types or GQL shapes covered by **`spec/lib/utils_spec.rb`** (or focused new spec files)
27
+ - Dependency range changes in **`contentstack_utils.gemspec`** justified and compatible with Ruby **≥ 3.1**
28
+
29
+ ### Minor (nice to have)
30
+
31
+ - **README.md** examples match actual class names (**`Options`**, not a non-existent **`Option`** unless aliased)
32
+ - YARD or comments only where they clarify non-obvious RTE or GQL mapping
33
+
34
+ ### Process
35
+
36
+ - Respect **CODEOWNERS** and branch policy (**feature/fix -> `development`**, release PRs **`development` -> `master`**) described in [dev-workflow](../dev-workflow/SKILL.md)
37
+
38
+ ## References
39
+
40
+ - [AGENTS.md](../../AGENTS.md)
41
+ - [Development workflow](../dev-workflow/SKILL.md)
42
+ - [Contentstack Utils SDK](../contentstack-utils/SKILL.md)
43
+ - [Framework & packaging](../framework/SKILL.md)
44
+ - [Testing](../testing/SKILL.md)
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: contentstack-utils
3
+ description: Use when changing the public Ruby API, RTE rendering (HTML string or JSON), GQL payloads, Model::Options/Metadata, or integration boundaries with the delivery client.
4
+ ---
5
+
6
+ # Contentstack Utils SDK – Contentstack Utils Ruby
7
+
8
+ ## When to use
9
+
10
+ - Implementing or fixing HTML output for rich text / JSON RTE
11
+ - Extending or subclassing rendering options (`ContentstackUtils::Model::Options`)
12
+ - Parsing GraphQL-shaped RTE (`ContentstackUtils::GQL`)
13
+ - Changing load paths or public entry points under `lib/` (gemspec and dependency ranges: see [framework](../framework/SKILL.md))
14
+
15
+ ## Instructions
16
+
17
+ ### Entry points
18
+
19
+ - **`lib/contentstack_utils.rb`** — requires version and `utils`
20
+ - **`lib/contentstack_utils/utils.rb`** — main module **`ContentstackUtils`** with class methods:
21
+ - **`render_content(content, options)`** — legacy HTML string RTE (array of strings or single string); uses Nokogiri and `_embedded_items` on the entry passed via options
22
+ - **`json_to_html(content, options)`** — JSON RTE document tree (Hash/Array) for CDA-style payloads with embedded items on the entry
23
+ - Internal helpers such as **`json_doc_to_html`** (used by GQL path)
24
+ - **`ContentstackUtils::GQL`** — **`json_to_html(content, options)`** for GraphQL responses: expects keys like `json` and optional `embedded_itemsConnection.edges`; reuses `ContentstackUtils.json_doc_to_html` with a GQL-specific reference resolver
25
+
26
+ ### Models and extension points
27
+
28
+ - **`ContentstackUtils::Model::Options`** (`lib/contentstack_utils/model/options.rb`) — default **`render_option`**, **`render_mark`**, **`render_node`**; subclass for custom HTML (see tests under `spec/mock/custom_render_option.rb`)
29
+ - **`ContentstackUtils::Model::Metadata`** — built from DOM nodes (HTML path) or JSON reference nodes
30
+ - **`ContentstackUtils::Interface::Rendarable`** (`lib/contentstack_utils/interface/renderable.rb`) — base for options; implement **`render_option`** in subclasses
31
+
32
+ ### Data contracts
33
+
34
+ - **CDA / delivery JSON path:** options may carry an **`entry`** hash with **`_embedded_items`** keyed by field UIDs
35
+ - **GQL path:** payload uses **`embedded_itemsConnection.edges`** with **`node`** objects; reference resolution matches **`metadata.item_uid`** to **`node.system.uid`**
36
+
37
+ ### Boundaries
38
+
39
+ - HTTP and stack access belong to the separate **Contentstack Ruby** client; this gem only renders given content + embedded metadata
40
+
41
+ ### Versioning
42
+
43
+ - Public API and behavior changes should be reflected in `CHANGELOG.md` and `lib/contentstack_utils/version.rb`
44
+
45
+ ## References
46
+
47
+ - Product overview: [README.md](../../README.md)
48
+ - [Framework & packaging](../framework/SKILL.md)
49
+ - [Testing](../testing/SKILL.md)
50
+ - [Ruby style and layout](../ruby-style/SKILL.md)
51
+ - [Contentstack documentation](https://www.contentstack.com/docs/)
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: dev-workflow
3
+ description: Use when setting up the dev environment, running build/test/docs, or understanding CI, branches, and gem release for this repo.
4
+ ---
5
+
6
+ # Development workflow – Contentstack Utils Ruby
7
+
8
+ ## When to use
9
+
10
+ - Cloning the repo or onboarding a new contributor
11
+ - Running tests, generating docs, or building the gem locally
12
+ - Understanding branch rules and GitHub Actions for this repository
13
+
14
+ ## Instructions
15
+
16
+ ### Prerequisites
17
+
18
+ - Ruby **≥ 3.1** (matches `s.required_ruby_version` in `contentstack_utils.gemspec`)
19
+ - Bundler; install gems with `bundle install`
20
+
21
+ ### Everyday commands
22
+
23
+ - **Run tests (default Rake task):** `bundle exec rake` or `bundle exec rake spec`
24
+ - **RSpec directly:** `bundle exec rspec` (pattern `spec/**/*_spec.rb` is configured in `Rakefile`)
25
+ - **YARD API docs:** `bundle exec rake yard` (see `.yardopts` for included paths)
26
+ - **Build gem artifact:** `gem build contentstack_utils.gemspec` (also used in `.github/workflows/release-gem.yml`)
27
+
28
+ ### Version and changelog
29
+
30
+ - Gem version lives in `lib/contentstack_utils/version.rb` as `ContentstackUtils::VERSION`
31
+ - Document user-visible changes in `CHANGELOG.md` when releasing
32
+
33
+ ### Branches and PRs
34
+
35
+ - Feature/fix PRs should target **`development`**. Release PRs are raised directly from **`development`** to **`master`**.
36
+ - Use `CODEOWNERS` for required reviewers when applicable
37
+
38
+ ### CI and automation (no RSpec workflow today)
39
+
40
+ - **Release:** `.github/workflows/release-gem.yml` — on GitHub **Release** created (`release: types: [created]`) for tag **`v*`** (draft releases skipped), checks out the tag, then builds and pushes to RubyGems (note: workflow pins Ruby 2.7 for publish; align with gemspec minimum when changing)
41
+ - **Security / compliance:** CodeQL, policy scan, SCA scan — see `.github/workflows/`
42
+ - **Issues:** Jira integration workflow in `.github/workflows/issues-jira.yml`
43
+
44
+ ### Optional housekeeping
45
+
46
+ - `.talismanrc` is used for secret scanning hooks in some environments; do not commit credentials or tokens
47
+
48
+ ## References
49
+
50
+ - [AGENTS.md](../../AGENTS.md)
51
+ - [Contentstack Utils SDK](../contentstack-utils/SKILL.md)
52
+ - [Framework & packaging](../framework/SKILL.md)
53
+ - [Testing](../testing/SKILL.md)
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: framework
3
+ description: Use when changing the gemspec, Bundler setup, Ruby/runtime constraints, or runtime dependencies (activesupport, nokogiri) and native extension implications.
4
+ ---
5
+
6
+ # Framework & packaging – Contentstack Utils Ruby
7
+
8
+ ## When to use
9
+
10
+ - Editing **`contentstack_utils.gemspec`** (dependencies, `required_ruby_version`, files list)
11
+ - Changing **`Gemfile`** / **`Gemfile.lock`** workflow or documenting install for contributors
12
+ - Evaluating impact of **nokogiri** (native extension) or **activesupport** version ranges on consumers
13
+
14
+ ## Instructions
15
+
16
+ ### Gem definition
17
+
18
+ - **`contentstack_utils.gemspec`** — canonical metadata: `s.files` from `git ls-files`, `spec/` as test files, `lib` as `require_paths`
19
+ - **`Gemfile`** — `gemspec` only; runtime and development dependencies are declared in the gemspec
20
+
21
+ ### Runtime dependencies (as shipped)
22
+
23
+ - **activesupport** `>= 7.0`, `< 8`
24
+ - **nokogiri** `>= 1.13`, `< 1.19` — HTML/XML parsing for legacy string RTE paths; consumers need a compatible platform build
25
+
26
+ ### Development dependencies
27
+
28
+ - **rake**, **rspec**, **webmock**, **simplecov**, **yard** — see gemspec; used for tasks in `Rakefile` and CI-style local checks
29
+
30
+ ### Ruby version
31
+
32
+ - **`s.required_ruby_version`** is **>= 3.1**; `.ruby-version` pins a team default for local dev
33
+ - **Release workflow** (`.github/workflows/release-gem.yml`) uses its own Ruby pin for `gem build` / `gem push`; keep it aligned with gemspec when updating
34
+
35
+ ### Build and publish
36
+
37
+ - Local artifact: `gem build contentstack_utils.gemspec`
38
+ - Publishing runs when a GitHub **Release** is created (`release: types: [created]`) for tag **`v*`** (draft releases skipped); see `release-gem.yml`.
39
+
40
+ ## References
41
+
42
+ - [Development workflow](../dev-workflow/SKILL.md)
43
+ - [Contentstack Utils SDK](../contentstack-utils/SKILL.md)
44
+ - [Ruby style and layout](../ruby-style/SKILL.md)
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: ruby-style
3
+ description: Use when editing Ruby in this repo—module layout, require paths, and staying consistent with existing style (not introducing a new linter config).
4
+ ---
5
+
6
+ # Ruby style and layout – Contentstack Utils Ruby
7
+
8
+ ## When to use
9
+
10
+ - Adding new `.rb` files under `lib/` or `spec/`
11
+ - Refactoring while keeping diffs readable for reviewers
12
+ - Deciding where a new helper or model class should live
13
+
14
+ ## Instructions
15
+
16
+ ### Layout
17
+
18
+ - **Library code:** `lib/contentstack_utils/` with nested folders `model/`, `interface/`, `support/`
19
+ - **Top-level require:** `lib/contentstack_utils.rb` should stay a thin loader
20
+ - **Specs:** mirror behavior under `spec/lib/` and `spec/mock/` / `spec/support/` as in existing examples
21
+
22
+ ### Conventions in this codebase
23
+
24
+ - **`ContentstackUtils`** as the root module; nested **`Model`**, **`Interface`**, **`GQL`** as already used
25
+ - **`require_relative`** for internal lib files (see `utils.rb`, `options.rb`)
26
+ - Existing files use a mix of spacing and `def self.` patterns; **prefer matching the surrounding file** over wholesale reformatting
27
+ - Typo in base class name **`Rendarable`** is historical; new code should remain compatible unless a dedicated rename is approved project-wide
28
+
29
+ ### Linting and format
30
+
31
+ - There is **no RuboCop or Standard** configuration in this repository; do not add large style-only churn unless the team adopts a formatter
32
+ - Run **`bundle exec rake spec`** before pushing substantive changes
33
+
34
+ ### Documentation
35
+
36
+ - YARD can document public APIs; keep user-facing usage examples in `README.md` accurate (e.g. class name **`Options`** vs informal “Option” in prose)
37
+
38
+ ## References
39
+
40
+ - [Contentstack Utils SDK](../contentstack-utils/SKILL.md)
41
+ - [Framework & packaging](../framework/SKILL.md)
42
+ - [Development workflow](../dev-workflow/SKILL.md)
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: testing
3
+ description: Use when writing or fixing RSpec tests, mocks, fixtures, SimpleCov filters, or WebMock usage in this repo.
4
+ ---
5
+
6
+ # Testing – Contentstack Utils Ruby
7
+
8
+ ## When to use
9
+
10
+ - Adding coverage for a new code path in `lib/`
11
+ - Debugging a failing spec in `spec/`
12
+ - Adjusting SimpleCov scope or shared test helpers
13
+
14
+ ## Instructions
15
+
16
+ ### Runner and config
17
+
18
+ - **RSpec** is the test framework; **`spec/spec_helper.rb`** loads SimpleCov, then `contentstack_utils`, mocks, and support files
19
+ - **Default task:** `bundle exec rake` runs **`spec`** with `--format documentation` (see `Rakefile`)
20
+
21
+ ### Layout
22
+
23
+ - **`spec/lib/`** — primary examples (`utils_spec.rb`, `model/option_spec.rb`, `model/metadata_spec.rb`)
24
+ - **`spec/mock/`** — constants and custom option subclasses used across examples
25
+ - **`spec/support/`** — helpers (e.g. XML/JSON fixture builders like `getGQLJSONRTE`)
26
+
27
+ ### Coverage
28
+
29
+ - **SimpleCov** starts in `spec_helper.rb` and **filters** `spec/` and `lib/contentstack_utils/support` from coverage metrics
30
+ - Aim to cover both **HTML `render_content`** paths and **JSON / GQL `json_to_html`** branches when changing `utils.rb`
31
+
32
+ ### HTTP and external I/O
33
+
34
+ - **webmock** is a development dependency; stub outbound HTTP if future tests introduce network calls (current suite is largely pure parsing/rendering)
35
+
36
+ ### Credentials
37
+
38
+ - Do not add real API keys, delivery tokens, or stack secrets to the repo; use fixture hashes and constants as in `spec/mock/` and `spec/support/constant.rb`
39
+
40
+ ## References
41
+
42
+ - [Development workflow](../dev-workflow/SKILL.md)
43
+ - [Contentstack Utils SDK](../contentstack-utils/SKILL.md)
44
+ - [RSpec](https://rspec.info/)
45
+ - [SimpleCov](https://github.com/simplecov-ruby/simplecov)
@@ -0,0 +1,268 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ContentstackUtils::Endpoint do
4
+ before(:each) do
5
+ described_class.reset_cache
6
+ end
7
+
8
+ # ── Default region ──────────────────────────────────────────────────────────
9
+
10
+ describe '.get_contentstack_endpoint with defaults' do
11
+ it 'returns a hash when no service is given' do
12
+ result = described_class.get_contentstack_endpoint
13
+ expect(result).to be_a(Hash)
14
+ end
15
+
16
+ it 'default region includes contentDelivery and contentManagement' do
17
+ result = described_class.get_contentstack_endpoint
18
+ expect(result).to have_key('contentDelivery')
19
+ expect(result).to have_key('contentManagement')
20
+ end
21
+
22
+ it 'default region contentDelivery is NA CDN' do
23
+ result = described_class.get_contentstack_endpoint
24
+ expect(result['contentDelivery']).to eq('https://cdn.contentstack.io')
25
+ end
26
+ end
27
+
28
+ # ── NA aliases ──────────────────────────────────────────────────────────────
29
+
30
+ describe 'NA region aliases' do
31
+ %w[na us aws-na aws_na NA US AWS-NA AWS_NA].each do |alias_val|
32
+ it "alias \"#{alias_val}\" resolves contentDelivery to NA CDN" do
33
+ result = described_class.get_contentstack_endpoint(region: alias_val, service: 'contentDelivery')
34
+ expect(result).to eq('https://cdn.contentstack.io')
35
+ end
36
+ end
37
+ end
38
+
39
+ # ── All 7 regions – contentDelivery ─────────────────────────────────────────
40
+
41
+ describe 'contentDelivery endpoints per region' do
42
+ {
43
+ 'na' => 'https://cdn.contentstack.io',
44
+ 'eu' => 'https://eu-cdn.contentstack.com',
45
+ 'au' => 'https://au-cdn.contentstack.com',
46
+ 'azure-na' => 'https://azure-na-cdn.contentstack.com',
47
+ 'azure-eu' => 'https://azure-eu-cdn.contentstack.com',
48
+ 'gcp-na' => 'https://gcp-na-cdn.contentstack.com',
49
+ 'gcp-eu' => 'https://gcp-eu-cdn.contentstack.com'
50
+ }.each do |region, expected_url|
51
+ it "region \"#{region}\" resolves contentDelivery to #{expected_url}" do
52
+ result = described_class.get_contentstack_endpoint(region: region, service: 'contentDelivery')
53
+ expect(result).to eq(expected_url)
54
+ end
55
+ end
56
+ end
57
+
58
+ # ── All 7 regions – contentManagement ───────────────────────────────────────
59
+
60
+ describe 'contentManagement endpoints per region' do
61
+ {
62
+ 'na' => 'https://api.contentstack.io',
63
+ 'eu' => 'https://eu-api.contentstack.com',
64
+ 'au' => 'https://au-api.contentstack.com',
65
+ 'azure-na' => 'https://azure-na-api.contentstack.com',
66
+ 'azure-eu' => 'https://azure-eu-api.contentstack.com',
67
+ 'gcp-na' => 'https://gcp-na-api.contentstack.com',
68
+ 'gcp-eu' => 'https://gcp-eu-api.contentstack.com'
69
+ }.each do |region, expected_url|
70
+ it "region \"#{region}\" resolves contentManagement to #{expected_url}" do
71
+ result = described_class.get_contentstack_endpoint(region: region, service: 'contentManagement')
72
+ expect(result).to eq(expected_url)
73
+ end
74
+ end
75
+ end
76
+
77
+ # ── Service keys present ─────────────────────────────────────────────────────
78
+
79
+ describe 'all service keys present' do
80
+ let(:expected_services) do
81
+ %w[
82
+ application contentDelivery contentManagement auth
83
+ graphqlDelivery preview graphqlPreview images assets
84
+ automate launch developerHub brandKit genAI
85
+ personalizeManagement personalizeEdge composableStudio
86
+ ]
87
+ end
88
+
89
+ it 'EU region returns all expected service keys' do
90
+ result = described_class.get_contentstack_endpoint(region: 'eu')
91
+ expected_services.each do |svc|
92
+ expect(result).to have_key(svc), "expected key #{svc} to be present"
93
+ end
94
+ end
95
+
96
+ it 'NA region additionally includes assetManagement' do
97
+ result = described_class.get_contentstack_endpoint(region: 'na')
98
+ expect(result).to have_key('assetManagement')
99
+ end
100
+ end
101
+
102
+ # ── omit_https flag ──────────────────────────────────────────────────────────
103
+
104
+ describe 'omit_https option' do
105
+ it 'strips https:// from a single service URL' do
106
+ result = described_class.get_contentstack_endpoint(region: 'na', service: 'contentDelivery', omit_https: true)
107
+ expect(result).to eq('cdn.contentstack.io')
108
+ end
109
+
110
+ it 'strips https:// from all endpoints when no service given' do
111
+ result = described_class.get_contentstack_endpoint(region: 'na', omit_https: true)
112
+ result.each_value do |url|
113
+ expect(url).not_to start_with('https://')
114
+ expect(url).not_to start_with('http://')
115
+ end
116
+ end
117
+
118
+ it 'retains https:// when omit_https is false (default)' do
119
+ result = described_class.get_contentstack_endpoint(region: 'na', service: 'contentDelivery', omit_https: false)
120
+ expect(result).to start_with('https://')
121
+ end
122
+
123
+ it 'strips https:// from EU contentManagement' do
124
+ result = described_class.get_contentstack_endpoint(region: 'eu', service: 'contentManagement', omit_https: true)
125
+ expect(result).to eq('eu-api.contentstack.com')
126
+ end
127
+ end
128
+
129
+ # ── Case-insensitive aliases ─────────────────────────────────────────────────
130
+
131
+ describe 'case-insensitive alias resolution' do
132
+ it 'resolves uppercase AWS-NA alias' do
133
+ result = described_class.get_contentstack_endpoint(region: 'AWS-NA', service: 'contentDelivery')
134
+ expect(result).to eq('https://azure-na-cdn.contentstack.com').or eq('https://cdn.contentstack.io')
135
+ end
136
+
137
+ it 'resolves azure_na alias' do
138
+ result = described_class.get_contentstack_endpoint(region: 'azure_na', service: 'contentDelivery')
139
+ expect(result).to eq('https://azure-na-cdn.contentstack.com')
140
+ end
141
+
142
+ it 'resolves gcp_eu alias' do
143
+ result = described_class.get_contentstack_endpoint(region: 'gcp_eu', service: 'contentDelivery')
144
+ expect(result).to eq('https://gcp-eu-cdn.contentstack.com')
145
+ end
146
+
147
+ it 'resolves AZURE-EU alias' do
148
+ result = described_class.get_contentstack_endpoint(region: 'AZURE-EU', service: 'contentDelivery')
149
+ expect(result).to eq('https://azure-eu-cdn.contentstack.com')
150
+ end
151
+
152
+ it 'resolves GCP-NA alias' do
153
+ result = described_class.get_contentstack_endpoint(region: 'GCP-NA', service: 'contentDelivery')
154
+ expect(result).to eq('https://gcp-na-cdn.contentstack.com')
155
+ end
156
+ end
157
+
158
+ # ── No-service returns full endpoints hash ───────────────────────────────────
159
+
160
+ describe 'full endpoints map returned when no service specified' do
161
+ it 'AU returns a hash with more than 1 entry' do
162
+ result = described_class.get_contentstack_endpoint(region: 'au')
163
+ expect(result).to be_a(Hash)
164
+ expect(result.size).to be > 1
165
+ end
166
+
167
+ it 'AU contentDelivery is correct in the map' do
168
+ result = described_class.get_contentstack_endpoint(region: 'au')
169
+ expect(result['contentDelivery']).to eq('https://au-cdn.contentstack.com')
170
+ end
171
+
172
+ it 'returns a copy — mutations do not affect cached data' do
173
+ result = described_class.get_contentstack_endpoint(region: 'na')
174
+ result['contentDelivery'] = 'mutated'
175
+ fresh = described_class.get_contentstack_endpoint(region: 'na')
176
+ expect(fresh['contentDelivery']).to eq('https://cdn.contentstack.io')
177
+ end
178
+ end
179
+
180
+ # ── Error cases ──────────────────────────────────────────────────────────────
181
+
182
+ describe 'error cases' do
183
+ it 'raises ArgumentError for empty string region' do
184
+ expect { described_class.get_contentstack_endpoint(region: '') }
185
+ .to raise_error(ArgumentError, 'Empty region provided')
186
+ end
187
+
188
+ it 'raises ArgumentError for whitespace-only region' do
189
+ expect { described_class.get_contentstack_endpoint(region: ' ') }
190
+ .to raise_error(ArgumentError, 'Empty region provided')
191
+ end
192
+
193
+ it 'raises ArgumentError for unknown region' do
194
+ expect { described_class.get_contentstack_endpoint(region: 'invalid-region') }
195
+ .to raise_error(ArgumentError, 'Invalid region: invalid-region')
196
+ end
197
+
198
+ it 'raises ArgumentError for unknown service' do
199
+ expect { described_class.get_contentstack_endpoint(region: 'na', service: 'unknownService') }
200
+ .to raise_error(ArgumentError, 'Service "unknownService" not found for region "na"')
201
+ end
202
+
203
+ it 'raises ArgumentError for valid region but unknown service' do
204
+ expect { described_class.get_contentstack_endpoint(region: 'eu', service: 'nonExistentService') }
205
+ .to raise_error(ArgumentError, /Service "nonExistentService" not found/)
206
+ end
207
+ end
208
+
209
+ # ── Additional service spot-checks ──────────────────────────────────────────
210
+
211
+ describe 'additional service endpoints' do
212
+ it 'resolves auth endpoint for NA' do
213
+ result = described_class.get_contentstack_endpoint(region: 'na', service: 'auth')
214
+ expect(result).to eq('https://auth-api.contentstack.com')
215
+ end
216
+
217
+ it 'resolves graphqlDelivery endpoint for EU' do
218
+ result = described_class.get_contentstack_endpoint(region: 'eu', service: 'graphqlDelivery')
219
+ expect(result).to eq('https://eu-graphql.contentstack.com')
220
+ end
221
+
222
+ it 'resolves preview endpoint for azure-na' do
223
+ result = described_class.get_contentstack_endpoint(region: 'azure-na', service: 'preview')
224
+ expect(result).to eq('https://azure-na-rest-preview.contentstack.com')
225
+ end
226
+
227
+ it 'resolves application endpoint for gcp-eu' do
228
+ result = described_class.get_contentstack_endpoint(region: 'gcp-eu', service: 'application')
229
+ expect(result).to eq('https://gcp-eu-app.contentstack.com')
230
+ end
231
+ end
232
+ end
233
+
234
+ # ── Utils proxy ────────────────────────────────────────────────────────────────
235
+
236
+ RSpec.describe ContentstackUtils do
237
+ before(:each) do
238
+ ContentstackUtils::Endpoint.reset_cache
239
+ end
240
+
241
+ describe '.get_contentstack_endpoint' do
242
+ it 'returns same value as Endpoint.get_contentstack_endpoint for default args' do
243
+ expect(described_class.get_contentstack_endpoint)
244
+ .to eq(ContentstackUtils::Endpoint.get_contentstack_endpoint)
245
+ end
246
+
247
+ it 'proxy returns contentDelivery for NA' do
248
+ result = described_class.get_contentstack_endpoint(region: 'na', service: 'contentDelivery')
249
+ expect(result).to eq('https://cdn.contentstack.io')
250
+ end
251
+
252
+ it 'proxy respects omit_https option' do
253
+ result = described_class.get_contentstack_endpoint(region: 'na', service: 'contentDelivery', omit_https: true)
254
+ expect(result).to eq('cdn.contentstack.io')
255
+ end
256
+
257
+ it 'proxy returns full endpoints map when no service given' do
258
+ result = described_class.get_contentstack_endpoint(region: 'eu')
259
+ expect(result).to be_a(Hash)
260
+ expect(result['contentManagement']).to eq('https://eu-api.contentstack.com')
261
+ end
262
+
263
+ it 'proxy raises ArgumentError for invalid region' do
264
+ expect { described_class.get_contentstack_endpoint(region: 'bad-region') }
265
+ .to raise_error(ArgumentError, /Invalid region/)
266
+ end
267
+ end
268
+ end