// Functional test for project cross-aspect duplication. // Mints an admin JWT (HS256, same secret/issuer/audience as identity-svc), // adds a scene to a real project, duplicates it to 1:1, verifies the clone has // the scene under a NEW project id + new aspect, then cleans up. import crypto from "node:crypto"; const GW = "http://172.28.144.1:8088"; const SECRET = "p9Xv7Lm2Qq8Nz4TfKc1Hs6YwRe3Ud0BafwefWEFw324234QEWF"; const b64url = (b) => Buffer.from(b).toString("base64url"); function mintToken() { const header = b64url(JSON.stringify({ alg: "HS256", typ: "JWT" })); const now = Math.floor(Date.now() / 1000); const payload = b64url(JSON.stringify({ sub: "00000000-0000-0000-0000-0000000000aa", jti: crypto.randomUUID(), tenant_id: "00000000-0000-0000-0000-0000000000bb", tenant_slug: "flatrender", is_admin: "true", is_tenant_admin: "false", role: "Admin", iss: "flatrender-identity", aud: "flatrender", exp: now + 3600, iat: now, })); const sig = crypto.createHmac("sha256", SECRET).update(`${header}.${payload}`).digest("base64url"); return `${header}.${payload}.${sig}`; } const H = { "Content-Type": "application/json", Authorization: `Bearer ${mintToken()}` }; async function j(method, path, body) { const res = await fetch(GW + path, { method, headers: H, body: body ? JSON.stringify(body) : undefined, redirect: "follow" }); const text = await res.text(); let data; try { data = text ? JSON.parse(text) : null; } catch { data = text; } return { status: res.status, data }; } // 1. find a project const pl = await j("GET", "/v1/projects/?page=1&page_size=1"); const items = pl.data?.items ?? pl.data?.data ?? (Array.isArray(pl.data) ? pl.data : []); const pid = items[0]?.id; console.log("source project:", pid ?? ""); if (!pid) { console.log("NO PROJECT — create one in admin first."); process.exit(0); } // 2. add a test scene to source const SK = "scene_dup_TEST"; console.log("add test scene:", (await j("POST", "/v1/scenes", { project_id: pid, key: SK, title: "Dup Test Scene", scene_type: "Normal", default_duration_sec: 4, overlap_at_end_sec: 0, can_handle_duration: true, generate_kf: false, manual_color_selection: false, sort: 99, is_active: true, })).status); // 3. duplicate to 1:1 const dup = await j("POST", `/v1/projects/${pid}/duplicate`, { aspect: "1:1", original_width: 1080, original_height: 1080, name: "DUP TEST 1:1" }); const newId = dup.data?.id; console.log("duplicate:", dup.status, "| new id:", newId, "| aspect:", dup.data?.aspect, "| width:", dup.data?.original_width); // 4. verify clone has the scene, scoped to the new project if (newId) { const r = await j("GET", `/v1/scenes/?project_id=${newId}`); const scenes = Array.isArray(r.data) ? r.data : r.data?.data ?? []; const found = scenes.find((s) => s.key === SK); console.log(`VERIFY → clone scenes: ${scenes.length} | test scene present: ${!!found} | scoped to new project: ${found?.project_id === newId} | new id != source: ${newId !== pid}`); } // 5. cleanup if (newId) console.log("cleanup dup project:", (await j("DELETE", `/v1/projects/${newId}`)).status); const ss = await j("GET", `/v1/scenes/?project_id=${pid}`); const srcScenes = Array.isArray(ss.data) ? ss.data : ss.data?.data ?? []; const ts = srcScenes.find((x) => x.key === SK); if (ts) console.log("cleanup test scene:", (await j("DELETE", `/v1/scenes/${ts.id}`)).status);