Skip to content

Commit 8182945

Browse files
feat: dynamic test stack with fixture seeding (no predefined stack required)
Opt-in via DYNAMIC_STACK=true: each run logs in, creates a fresh stack, creates a scoped management token, seeds baseline fixtures, runs the suite and deletes the stack afterwards - mirroring the JS CMA SDK sanity suite. Static mode (predefined stack from .env) is unchanged and remains default. - TestStackContext: stack lifecycle (login/create/token/teardown via JVM shutdown hook; teardown deliberately NOT in launcherSessionClosed - the vintage suite runner opens nested sessions whose close fires mid-run) - FixtureSeeder + TestDataRegistry: seeds fixtures/core (sanitized schema from the CDA-SDK-v10 export, no real UIDs) in dependency order with {{ref:...}} placeholder resolution - TestSuiteLifecycle (LauncherSessionListener) + TestClient bridge: API_KEY/MANAGEMENT_TOKEN/AUTHTOKEN resolve from the dynamic stack - APISanityTestSuite reordered by module dependency - Taxonomy tests and asset publish use the authtoken on dynamic stacks (API-created management tokens cannot scope taxonomy or publishing) - Tests that passed vacuously while the SDK dropped the authtoken now assert real behavior (header counts, stack vs stacks response shape) Results: dynamic 246/0 failures; static unchanged; units 280/0.
1 parent 0d1a8c7 commit 8182945

30 files changed

Lines changed: 4604 additions & 43 deletions

.talismanrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ fileignoreconfig:
66
- filecontent
77
- filename: .dccache
88
checksum: 56b07d2204dee34290e064faf1d244643c63998888d962d4f8b2d206d604c8d4
9+
- filename: src/test/java/com/contentstack/cms/TestStackContext.java
10+
checksum: 54119d0f9bdc14155a861e1d9219e06cd95d80fa8278dff02236ffc52b2c22a6
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
package com.contentstack.cms;
2+
3+
import okhttp3.MediaType;
4+
import okhttp3.MultipartBody;
5+
import okhttp3.OkHttpClient;
6+
import okhttp3.Request;
7+
import okhttp3.RequestBody;
8+
import okhttp3.Response;
9+
import org.json.simple.JSONArray;
10+
import org.json.simple.JSONObject;
11+
import org.json.simple.parser.JSONParser;
12+
13+
import java.io.File;
14+
import java.io.FileReader;
15+
import java.nio.file.Files;
16+
import java.nio.file.Path;
17+
import java.nio.file.Paths;
18+
import java.util.ArrayList;
19+
import java.util.Comparator;
20+
import java.util.List;
21+
import java.util.concurrent.TimeUnit;
22+
import java.util.regex.Matcher;
23+
import java.util.regex.Pattern;
24+
import java.util.stream.Collectors;
25+
import java.util.stream.Stream;
26+
27+
/**
28+
* Seeds the dynamically created test stack with baseline fixture data
29+
* (Phase 2 of the dynamic-fixtures plan — see docs/DYNAMIC-TEST-FIXTURES.md).
30+
*
31+
* <p>Fixtures live in {@code src/test/resources/fixtures/core/} and are
32+
* sanitized schema extracted from {@code docs/CDA-SDK-v10} (no real UIDs).
33+
* Seeding order follows module dependencies, mirroring the JS sanity suite:
34+
*
35+
* <pre>
36+
* locales → global fields (deepest nesting first) → content types
37+
* → taxonomies (+terms) → environments → assets → entries
38+
* </pre>
39+
*
40+
* <p>All calls use the <b>authtoken</b> (org-admin rights), so seeding is
41+
* unaffected by management-token scope limitations. Entry fixtures may use
42+
* placeholders resolved against {@link TestDataRegistry} at seed time:
43+
* <ul>
44+
* <li>{@code {{ref:<content_type_uid>:<index>}}} → Nth created entry UID</li>
45+
* <li>{@code {{asset:<index>}}} → Nth uploaded asset UID</li>
46+
* <li>{@code {{env:<name>}}} → environment UID</li>
47+
* </ul>
48+
*
49+
* <p>Failures are logged and non-fatal: an individual bad fixture must not
50+
* take down the run — affected tests will fail/skip on their own and point
51+
* at the gap.
52+
*/
53+
final class FixtureSeeder {
54+
55+
private static final Pattern PLACEHOLDER = Pattern.compile("\\{\\{(ref|asset|env):([^:}]+)(?::(\\d+))?}}");
56+
private static final MediaType JSON_MEDIA = MediaType.parse("application/json; charset=utf-8");
57+
58+
private final OkHttpClient http = new OkHttpClient.Builder()
59+
.connectTimeout(30, TimeUnit.SECONDS)
60+
.readTimeout(60, TimeUnit.SECONDS)
61+
.build();
62+
private final JSONParser parser = new JSONParser();
63+
64+
private final String host;
65+
private final String apiKey;
66+
private final String authtoken;
67+
private final Path fixturesDir;
68+
69+
private int created = 0;
70+
private int failed = 0;
71+
72+
FixtureSeeder(String host, String apiKey, String authtoken) {
73+
this.host = host;
74+
this.apiKey = apiKey;
75+
this.authtoken = authtoken;
76+
this.fixturesDir = Paths.get("src", "test", "resources", "fixtures", "core");
77+
}
78+
79+
/** Runs the full seeding sequence. Never throws. */
80+
void seedAll() {
81+
if (!Files.isDirectory(fixturesDir)) {
82+
System.err.println("[FixtureSeeder] fixtures dir not found: " + fixturesDir.toAbsolutePath());
83+
return;
84+
}
85+
long start = System.currentTimeMillis();
86+
System.out.println("[FixtureSeeder] Seeding fixtures from " + fixturesDir + " ...");
87+
seedLocales();
88+
seedGlobalFields();
89+
// Taxonomies BEFORE content types: CT schemas with taxonomy fields
90+
// validate the taxonomy uids at creation time (422 otherwise)
91+
seedTaxonomies();
92+
seedContentTypes();
93+
seedEnvironments();
94+
seedAssets();
95+
seedEntries();
96+
System.out.println("[FixtureSeeder] Done in " + (System.currentTimeMillis() - start) / 1000 + "s"
97+
+ " (created=" + created + ", failed=" + failed + ") -> " + TestDataRegistry.summary());
98+
}
99+
100+
// =============================================================================
101+
// Module seeding
102+
// =============================================================================
103+
104+
@SuppressWarnings("unchecked")
105+
private void seedLocales() {
106+
JSONObject root = readJson(fixturesDir.resolve("locales.json"));
107+
if (root == null) return;
108+
for (Object o : (JSONArray) root.get("locales")) {
109+
JSONObject locale = (JSONObject) o;
110+
JSONObject body = new JSONObject();
111+
body.put("locale", locale);
112+
JSONObject res = post("/v3/locales", body, "locale " + locale.get("code"));
113+
if (res != null) {
114+
JSONObject created = (JSONObject) res.get("locale");
115+
TestDataRegistry.recordLocale((String) locale.get("code"),
116+
created != null ? (String) created.get("uid") : null);
117+
}
118+
}
119+
}
120+
121+
@SuppressWarnings("unchecked")
122+
private void seedGlobalFields() {
123+
for (Path file : listSorted("global_fields")) {
124+
JSONObject body = readJson(file);
125+
if (body == null) continue;
126+
String uid = (String) ((JSONObject) body.get("global_field")).get("uid");
127+
JSONObject res = post("/v3/global_fields", body, "global field " + uid);
128+
if (res != null) {
129+
TestDataRegistry.recordGlobalField(uid);
130+
}
131+
}
132+
}
133+
134+
@SuppressWarnings("unchecked")
135+
private void seedContentTypes() {
136+
for (Path file : listSorted("content_types")) {
137+
JSONObject body = readJson(file);
138+
if (body == null) continue;
139+
String uid = (String) ((JSONObject) body.get("content_type")).get("uid");
140+
JSONObject res = post("/v3/content_types", body, "content type " + uid);
141+
if (res != null) {
142+
TestDataRegistry.recordContentType(uid);
143+
}
144+
}
145+
}
146+
147+
@SuppressWarnings("unchecked")
148+
private void seedTaxonomies() {
149+
for (Path file : listSorted("taxonomies")) {
150+
JSONObject root = readJson(file);
151+
if (root == null) continue;
152+
JSONObject taxonomy = (JSONObject) root.get("taxonomy");
153+
String uid = (String) taxonomy.get("uid");
154+
JSONObject body = new JSONObject();
155+
body.put("taxonomy", taxonomy);
156+
JSONObject res = post("/v3/taxonomies", body, "taxonomy " + uid);
157+
if (res == null) continue;
158+
TestDataRegistry.recordTaxonomy(uid);
159+
JSONArray terms = (JSONArray) root.get("terms");
160+
if (terms == null) continue;
161+
for (Object o : terms) {
162+
JSONObject term = (JSONObject) o;
163+
JSONObject termBody = new JSONObject();
164+
termBody.put("term", term);
165+
post("/v3/taxonomies/" + uid + "/terms", termBody,
166+
"term " + uid + "/" + term.get("uid"));
167+
}
168+
}
169+
}
170+
171+
@SuppressWarnings("unchecked")
172+
private void seedEnvironments() {
173+
JSONObject root = readJson(fixturesDir.resolve("environments.json"));
174+
if (root == null) return;
175+
for (Object o : (JSONArray) root.get("environments")) {
176+
JSONObject environment = (JSONObject) o;
177+
JSONObject body = new JSONObject();
178+
body.put("environment", environment);
179+
String name = (String) environment.get("name");
180+
JSONObject res = post("/v3/environments", body, "environment " + name);
181+
if (res != null) {
182+
JSONObject created = (JSONObject) res.get("environment");
183+
TestDataRegistry.recordEnvironment(name,
184+
created != null ? (String) created.get("uid") : null);
185+
}
186+
}
187+
}
188+
189+
/** Uploads the repo's standard test asset so entries/tests have one available. */
190+
private void seedAssets() {
191+
File assetFile = new File("src/test/resources/asset.png");
192+
if (!assetFile.exists()) {
193+
System.err.println("[FixtureSeeder] asset file missing: " + assetFile.getPath());
194+
return;
195+
}
196+
try {
197+
RequestBody fileBody = RequestBody.create(assetFile, MediaType.parse("image/png"));
198+
MultipartBody multipart = new MultipartBody.Builder()
199+
.setType(MultipartBody.FORM)
200+
.addFormDataPart("asset[upload]", assetFile.getName(), fileBody)
201+
.addFormDataPart("asset[title]", "Seeded test asset")
202+
.addFormDataPart("asset[description]", "Uploaded by FixtureSeeder")
203+
.build();
204+
Request request = new Request.Builder()
205+
.url("https://" + host + "/v3/assets")
206+
.header("api_key", apiKey)
207+
.header("authtoken", authtoken)
208+
.post(multipart)
209+
.build();
210+
try (Response response = http.newCall(request).execute()) {
211+
String responseBody = response.body() != null ? response.body().string() : "";
212+
if (response.isSuccessful()) {
213+
JSONObject json = (JSONObject) parser.parse(responseBody);
214+
JSONObject asset = (JSONObject) json.get("asset");
215+
TestDataRegistry.recordAsset((String) asset.get("uid"));
216+
created++;
217+
System.out.println("[FixtureSeeder] + asset " + asset.get("uid"));
218+
} else {
219+
failed++;
220+
System.err.println("[FixtureSeeder] ! asset upload failed (" + response.code() + "): "
221+
+ truncate(responseBody));
222+
}
223+
}
224+
} catch (Exception e) {
225+
failed++;
226+
System.err.println("[FixtureSeeder] ! asset upload error: " + e.getMessage());
227+
}
228+
}
229+
230+
@SuppressWarnings("unchecked")
231+
private void seedEntries() {
232+
for (Path file : listSorted("entries")) {
233+
JSONObject root = readJson(file);
234+
if (root == null) continue;
235+
String contentType = (String) root.get("content_type");
236+
for (Object o : (JSONArray) root.get("entries")) {
237+
JSONObject entry = (JSONObject) o;
238+
String resolved = resolvePlaceholders(entry.toJSONString());
239+
JSONObject body = new JSONObject();
240+
try {
241+
body.put("entry", parser.parse(resolved));
242+
} catch (Exception e) {
243+
failed++;
244+
System.err.println("[FixtureSeeder] ! bad entry fixture in " + file + ": " + e.getMessage());
245+
continue;
246+
}
247+
JSONObject res = post("/v3/content_types/" + contentType + "/entries?locale=en-us",
248+
body, "entry " + contentType + "/" + entry.get("title"));
249+
if (res != null) {
250+
JSONObject created = (JSONObject) res.get("entry");
251+
if (created != null) {
252+
TestDataRegistry.recordEntry(contentType, (String) created.get("uid"));
253+
}
254+
}
255+
}
256+
}
257+
}
258+
259+
// =============================================================================
260+
// Helpers
261+
// =============================================================================
262+
263+
/** Resolves {{ref:ct:n}}, {{asset:n}} and {{env:name}} placeholders. */
264+
private String resolvePlaceholders(String json) {
265+
Matcher m = PLACEHOLDER.matcher(json);
266+
StringBuffer sb = new StringBuffer();
267+
while (m.find()) {
268+
String kind = m.group(1);
269+
String key = m.group(2);
270+
String idx = m.group(3);
271+
String value = null;
272+
switch (kind) {
273+
case "ref":
274+
value = TestDataRegistry.entryUid(key, idx != null ? Integer.parseInt(idx) : 0);
275+
break;
276+
case "asset":
277+
value = TestDataRegistry.assetUid(Integer.parseInt(key));
278+
break;
279+
case "env":
280+
value = TestDataRegistry.environmentUid(key);
281+
break;
282+
default:
283+
break;
284+
}
285+
m.appendReplacement(sb, Matcher.quoteReplacement(value != null ? value : m.group(0)));
286+
}
287+
m.appendTail(sb);
288+
return sb.toString();
289+
}
290+
291+
/** POST helper: returns parsed response JSON on 2xx, null otherwise (logged). */
292+
private JSONObject post(String path, JSONObject body, String label) {
293+
try {
294+
Request request = new Request.Builder()
295+
.url("https://" + host + path)
296+
.header("api_key", apiKey)
297+
.header("authtoken", authtoken)
298+
.post(RequestBody.create(body.toJSONString(), JSON_MEDIA))
299+
.build();
300+
try (Response response = http.newCall(request).execute()) {
301+
String responseBody = response.body() != null ? response.body().string() : "";
302+
if (response.isSuccessful()) {
303+
created++;
304+
System.out.println("[FixtureSeeder] + " + label);
305+
return (JSONObject) parser.parse(responseBody);
306+
}
307+
failed++;
308+
System.err.println("[FixtureSeeder] ! " + label + " failed (" + response.code() + "): "
309+
+ truncate(responseBody));
310+
return null;
311+
}
312+
} catch (Exception e) {
313+
failed++;
314+
System.err.println("[FixtureSeeder] ! " + label + " error: " + e.getMessage());
315+
return null;
316+
}
317+
}
318+
319+
private JSONObject readJson(Path file) {
320+
try (FileReader reader = new FileReader(file.toFile())) {
321+
return (JSONObject) parser.parse(reader);
322+
} catch (Exception e) {
323+
failed++;
324+
System.err.println("[FixtureSeeder] ! cannot read " + file + ": " + e.getMessage());
325+
return null;
326+
}
327+
}
328+
329+
/** Lists *.json files in a fixtures subdirectory, sorted by filename (numeric prefixes = order). */
330+
private List<Path> listSorted(String subdir) {
331+
Path dir = fixturesDir.resolve(subdir);
332+
if (!Files.isDirectory(dir)) {
333+
return new ArrayList<>();
334+
}
335+
try (Stream<Path> stream = Files.list(dir)) {
336+
return stream.filter(p -> p.toString().endsWith(".json"))
337+
.sorted(Comparator.comparing(p -> p.getFileName().toString()))
338+
.collect(Collectors.toList());
339+
} catch (Exception e) {
340+
System.err.println("[FixtureSeeder] ! cannot list " + dir + ": " + e.getMessage());
341+
return new ArrayList<>();
342+
}
343+
}
344+
345+
private static String truncate(String s) {
346+
return s.length() > 160 ? s.substring(0, 160) + "..." : s;
347+
}
348+
}

0 commit comments

Comments
 (0)