游戏分发 E2E 抽出共享 multipart 发布 helper
- 新增 scripts/game-distribution-e2e-helpers.mjs:COVER_PNG、imagePart、coverImagePart、publishFormData、resolveApiRequest - 11 个 E2E 脚本删除各自的重复实现,改为 import;api() 的 body 分支收敛到 resolveApiRequest - 各脚本的封面文件名前缀、envelope 与鉴权头合并顺序、返回形状保持不变
This commit is contained in:
@@ -15,6 +15,12 @@ import { readFile } from 'node:fs/promises';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
import {
|
||||
imagePart,
|
||||
publishFormData,
|
||||
resolveApiRequest,
|
||||
} from './game-distribution-e2e-helpers.mjs';
|
||||
|
||||
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:12401';
|
||||
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
|
||||
const ADMIN_USER = (process.env.E2E_ADMIN_USER ?? '').trim();
|
||||
@@ -26,10 +32,6 @@ if (!ADMIN_USER || !ADMIN_PASSWORD) {
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
const COVER_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
|
||||
let failures = 0;
|
||||
function check(name, ok, detail = '') {
|
||||
@@ -71,20 +73,16 @@ async function api(path, options = {}) {
|
||||
} = options;
|
||||
const finalHeaders = { ...ENVELOPE, ...headers };
|
||||
if (token) finalHeaders.Authorization = `Bearer ${token}`;
|
||||
let finalBody;
|
||||
if (formData) {
|
||||
// multipart/form-data:不手动设 Content-Type,交给 fetch 生成 boundary。
|
||||
finalBody = formData;
|
||||
} else if (binary) {
|
||||
finalBody = binary;
|
||||
} else if (body !== undefined) {
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
finalBody = JSON.stringify(body);
|
||||
}
|
||||
const request = resolveApiRequest({
|
||||
formData,
|
||||
binary,
|
||||
body,
|
||||
headers: finalHeaders,
|
||||
});
|
||||
const response = await fetch(`${API}${path}`, {
|
||||
method,
|
||||
headers: finalHeaders,
|
||||
body: finalBody,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
@@ -106,47 +104,6 @@ function stamp() {
|
||||
return `${Date.now()}${Math.floor(Math.random() * 1000)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布用图片 part。
|
||||
*
|
||||
* 新合同不再走素材库直传登记:图片字节随 `cover` / `screenshot` part 提交,由服务端直写
|
||||
* 项目快照桶并派生 objectKey。这里只在本机准备字节、文件名与 content-type。
|
||||
*/
|
||||
function imagePart(kind, id, bytes = COVER_PNG) {
|
||||
return {
|
||||
bytes,
|
||||
fileName: `${kind}-${id}.png`,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装发布请求体:`metadata` 文本 part + 可选 `cover` / 按顺序重复的 `screenshot` 二进制 part。
|
||||
*
|
||||
* `metadata.screenshots` 里的 `null` 槽位按出现顺序消费 `screenshots` 数组里的 part;string
|
||||
* 槽位沿用该 objectKey。调用方负责保证 null 槽位数量与 part 数量一致(不一致服务端会 400)。
|
||||
*/
|
||||
function publishFormData(metadata, { cover, screenshots = [] } = {}) {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
if (cover) {
|
||||
form.append(
|
||||
'cover',
|
||||
new Blob([cover.bytes], { type: cover.contentType }),
|
||||
cover.fileName,
|
||||
);
|
||||
}
|
||||
for (const screenshot of screenshots) {
|
||||
if (!screenshot) continue;
|
||||
form.append(
|
||||
'screenshot',
|
||||
new Blob([screenshot.bytes], { type: screenshot.contentType }),
|
||||
screenshot.fileName,
|
||||
);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
|
||||
function gameMetadata(overrides = {}) {
|
||||
return {
|
||||
title: `分发媒体验证 ${stamp().slice(-6)}`,
|
||||
|
||||
@@ -23,10 +23,12 @@ import path from 'node:path';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const COVER_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
import {
|
||||
imagePart,
|
||||
publishFormData,
|
||||
resolveApiRequest,
|
||||
} from './game-distribution-e2e-helpers.mjs';
|
||||
|
||||
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:8082';
|
||||
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
|
||||
const ADMIN_USER = (process.env.E2E_ADMIN_USER ?? '').trim();
|
||||
@@ -71,20 +73,16 @@ async function api(pathname, options = {}) {
|
||||
} = options;
|
||||
const finalHeaders = { ...ENVELOPE, ...headers };
|
||||
if (token) finalHeaders.Authorization = `Bearer ${token}`;
|
||||
let finalBody;
|
||||
if (formData) {
|
||||
// multipart/form-data:不手动设 Content-Type,交给 fetch 生成 boundary。
|
||||
finalBody = formData;
|
||||
} else if (binary) {
|
||||
finalBody = binary;
|
||||
} else if (body !== undefined) {
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
finalBody = JSON.stringify(body);
|
||||
}
|
||||
const request = resolveApiRequest({
|
||||
formData,
|
||||
binary,
|
||||
body,
|
||||
headers: finalHeaders,
|
||||
});
|
||||
const response = await fetch(`${API}${pathname}`, {
|
||||
method,
|
||||
headers: finalHeaders,
|
||||
body: finalBody,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
@@ -172,36 +170,8 @@ async function setPublishGate(admin, enabled, rolloutPercent) {
|
||||
return response;
|
||||
}
|
||||
|
||||
/** 发布用封面图片 part:字节随 `cover` part 提交,服务端直写项目快照桶。 */
|
||||
function coverImagePart(id) {
|
||||
return {
|
||||
bytes: COVER_PNG,
|
||||
fileName: `ops-${id}.png`,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
}
|
||||
|
||||
/** 组装发布请求体:`metadata` 文本 part + 可选 `cover` / 重复 `screenshot` 二进制 part。 */
|
||||
function publishFormData(metadata, { cover, screenshots = [] } = {}) {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
if (cover) {
|
||||
form.append(
|
||||
'cover',
|
||||
new Blob([cover.bytes], { type: cover.contentType }),
|
||||
cover.fileName,
|
||||
);
|
||||
}
|
||||
for (const screenshot of screenshots) {
|
||||
if (!screenshot) continue;
|
||||
form.append(
|
||||
'screenshot',
|
||||
new Blob([screenshot.bytes], { type: screenshot.contentType }),
|
||||
screenshot.fileName,
|
||||
);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
const coverImagePart = (id) =>
|
||||
imagePart('cover', id, { fileName: `ops-${id}.png` });
|
||||
|
||||
function gameMetadata(title, coverObjectKey) {
|
||||
return {
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
// 的 my-games)→ 作者撤回自己的版本成功。整条链路只在本机 dev 数据库落行;唯一的存储写入
|
||||
// 是「建游戏必须提供封面」逼出的一个 67 字节 PNG(新合同由服务端把 cover part 直写项目
|
||||
// 快照桶),发行包分片与确认全部停在越权拒绝之前,不会产生任何对象。
|
||||
const COVER_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
import {
|
||||
imagePart,
|
||||
publishFormData,
|
||||
resolveApiRequest,
|
||||
} from './game-distribution-e2e-helpers.mjs';
|
||||
|
||||
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:4198';
|
||||
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
|
||||
const ADMIN_USER = (process.env.E2E_ADMIN_USER ?? '').trim();
|
||||
@@ -49,20 +51,16 @@ async function api(path, options = {}) {
|
||||
} = options;
|
||||
const finalHeaders = { ...ENVELOPE, ...headers };
|
||||
if (token) finalHeaders.Authorization = `Bearer ${token}`;
|
||||
let finalBody;
|
||||
if (formData) {
|
||||
// multipart/form-data:不手动设 Content-Type,交给 fetch 生成 boundary。
|
||||
finalBody = formData;
|
||||
} else if (binary) {
|
||||
finalBody = binary;
|
||||
} else if (body !== undefined) {
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
finalBody = JSON.stringify(body);
|
||||
}
|
||||
const request = resolveApiRequest({
|
||||
formData,
|
||||
binary,
|
||||
body,
|
||||
headers: finalHeaders,
|
||||
});
|
||||
const response = await fetch(`${API}${path}`, {
|
||||
method,
|
||||
headers: finalHeaders,
|
||||
body: finalBody,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
@@ -95,41 +93,8 @@ function gameMetadata(title) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布用封面图片 part。
|
||||
*
|
||||
* 新合同不再走素材库直传登记:封面字节随创建请求的 `cover` part 提交,由服务端直写项目
|
||||
* 快照桶。这里只在本机准备字节、文件名与 content-type。
|
||||
*/
|
||||
function coverImagePart(id) {
|
||||
return {
|
||||
bytes: COVER_PNG,
|
||||
fileName: `owner-isolation-${id}.png`,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
}
|
||||
|
||||
/** 组装发布请求体:`metadata` 文本 part + 可选 `cover` / 重复 `screenshot` 二进制 part。 */
|
||||
function publishFormData(metadata, { cover, screenshots = [] } = {}) {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
if (cover) {
|
||||
form.append(
|
||||
'cover',
|
||||
new Blob([cover.bytes], { type: cover.contentType }),
|
||||
cover.fileName,
|
||||
);
|
||||
}
|
||||
for (const screenshot of screenshots) {
|
||||
if (!screenshot) continue;
|
||||
form.append(
|
||||
'screenshot',
|
||||
new Blob([screenshot.bytes], { type: screenshot.contentType }),
|
||||
screenshot.fileName,
|
||||
);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
const coverImagePart = (id) =>
|
||||
imagePart('cover', id, { fileName: `owner-isolation-${id}.png` });
|
||||
|
||||
async function register(prefix) {
|
||||
const response = await api('/api/auth/entry', {
|
||||
|
||||
@@ -9,10 +9,12 @@ import { createHash, randomBytes } from 'node:crypto';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const COVER_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
import {
|
||||
imagePart,
|
||||
publishFormData,
|
||||
resolveApiRequest,
|
||||
} from './game-distribution-e2e-helpers.mjs';
|
||||
|
||||
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:8082';
|
||||
const API_PORT = Number(new URL(API).port || '80');
|
||||
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
|
||||
@@ -45,20 +47,16 @@ async function api(pathname, options = {}) {
|
||||
} = options;
|
||||
const finalHeaders = { ...ENVELOPE, ...headers };
|
||||
if (token) finalHeaders.Authorization = `Bearer ${token}`;
|
||||
let finalBody;
|
||||
if (formData) {
|
||||
// multipart/form-data:不手动设 Content-Type,交给 fetch 生成 boundary。
|
||||
finalBody = formData;
|
||||
} else if (binary) {
|
||||
finalBody = binary;
|
||||
} else if (body !== undefined) {
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
finalBody = JSON.stringify(body);
|
||||
}
|
||||
const request = resolveApiRequest({
|
||||
formData,
|
||||
binary,
|
||||
body,
|
||||
headers: finalHeaders,
|
||||
});
|
||||
const response = await fetch(`${API}${pathname}`, {
|
||||
method,
|
||||
headers: finalHeaders,
|
||||
body: finalBody,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
@@ -90,36 +88,8 @@ function apiServerWorkingSetMb() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 发布用封面图片 part:字节随 `cover` part 提交,服务端直写项目快照桶。 */
|
||||
function coverImagePart(id) {
|
||||
return {
|
||||
bytes: COVER_PNG,
|
||||
fileName: `limits-${id}.png`,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
}
|
||||
|
||||
/** 组装发布请求体:`metadata` 文本 part + 可选 `cover` / 重复 `screenshot` 二进制 part。 */
|
||||
function publishFormData(metadata, { cover, screenshots = [] } = {}) {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
if (cover) {
|
||||
form.append(
|
||||
'cover',
|
||||
new Blob([cover.bytes], { type: cover.contentType }),
|
||||
cover.fileName,
|
||||
);
|
||||
}
|
||||
for (const screenshot of screenshots) {
|
||||
if (!screenshot) continue;
|
||||
form.append(
|
||||
'screenshot',
|
||||
new Blob([screenshot.bytes], { type: screenshot.contentType }),
|
||||
screenshot.fileName,
|
||||
);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
const coverImagePart = (id) =>
|
||||
imagePart('cover', id, { fileName: `limits-${id}.png` });
|
||||
|
||||
async function createGameWithVersion(
|
||||
author,
|
||||
|
||||
@@ -51,6 +51,12 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
import {
|
||||
imagePart,
|
||||
publishFormData,
|
||||
resolveApiRequest,
|
||||
} from './game-distribution-e2e-helpers.mjs';
|
||||
|
||||
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
|
||||
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const DEV_STACK_PATH = resolve(REPO_ROOT, '.app/dev-stack.json');
|
||||
@@ -88,12 +94,6 @@ const REQUIRE_PLATFORM_ORIGIN =
|
||||
(process.env.E2E_REQUIRE_PLATFORM_ORIGIN ?? '').trim() === '1';
|
||||
const GAME_DISTRIBUTION_PURCHASE_SOURCE_TYPE = 'game_purchase';
|
||||
|
||||
/** 1x1 透明 PNG:仅用于满足发布必填封面,内容不影响本用例断言。 */
|
||||
const COVER_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
|
||||
let failures = 0;
|
||||
let passes = 0;
|
||||
let warnings = 0;
|
||||
@@ -177,20 +177,16 @@ async function api(path, options = {}) {
|
||||
} = options;
|
||||
const finalHeaders = { ...ENVELOPE, ...headers };
|
||||
if (token) finalHeaders.Authorization = `Bearer ${token}`;
|
||||
let finalBody;
|
||||
if (formData) {
|
||||
// multipart/form-data:不手动设 Content-Type,交给 fetch 生成 boundary。
|
||||
finalBody = formData;
|
||||
} else if (binary) {
|
||||
finalBody = binary;
|
||||
} else if (body !== undefined) {
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
finalBody = JSON.stringify(body);
|
||||
}
|
||||
const request = resolveApiRequest({
|
||||
formData,
|
||||
binary,
|
||||
body,
|
||||
headers: finalHeaders,
|
||||
});
|
||||
const response = await fetch(`${API}${path}`, {
|
||||
method,
|
||||
headers: finalHeaders,
|
||||
body: finalBody,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
@@ -223,36 +219,8 @@ async function registerAccount(prefix) {
|
||||
return { status: entry.status, token: entry.data?.token ?? '', phone };
|
||||
}
|
||||
|
||||
/** 发布用封面图片 part:字节随 `cover` part 提交,服务端直写项目快照桶。 */
|
||||
function coverImagePart(id) {
|
||||
return {
|
||||
bytes: COVER_PNG,
|
||||
fileName: `cover-${id}.png`,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
}
|
||||
|
||||
/** 组装发布请求体:`metadata` 文本 part + 可选 `cover` / 重复 `screenshot` 二进制 part。 */
|
||||
function publishFormData(metadata, { cover, screenshots = [] } = {}) {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
if (cover) {
|
||||
form.append(
|
||||
'cover',
|
||||
new Blob([cover.bytes], { type: cover.contentType }),
|
||||
cover.fileName,
|
||||
);
|
||||
}
|
||||
for (const screenshot of screenshots) {
|
||||
if (!screenshot) continue;
|
||||
form.append(
|
||||
'screenshot',
|
||||
new Blob([screenshot.bytes], { type: screenshot.contentType }),
|
||||
screenshot.fileName,
|
||||
);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
const coverImagePart = (id) =>
|
||||
imagePart('cover', id, { fileName: `cover-${id}.png` });
|
||||
|
||||
/**
|
||||
* 生成最小可玩发行包:入口页里带唯一标记,用来证明播放会话读到的是同一份包内容。
|
||||
|
||||
@@ -15,10 +15,12 @@ import path from 'node:path';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const COVER_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
import {
|
||||
imagePart,
|
||||
publishFormData,
|
||||
resolveApiRequest,
|
||||
} from './game-distribution-e2e-helpers.mjs';
|
||||
|
||||
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:4198';
|
||||
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
|
||||
const ADMIN_USER = (process.env.E2E_ADMIN_USER ?? '').trim();
|
||||
@@ -51,20 +53,16 @@ async function api(pathname, options = {}) {
|
||||
} = options;
|
||||
const finalHeaders = { ...ENVELOPE, ...headers };
|
||||
if (token) finalHeaders.Authorization = `Bearer ${token}`;
|
||||
let finalBody;
|
||||
if (formData) {
|
||||
// multipart/form-data:不手动设 Content-Type,交给 fetch 生成 boundary。
|
||||
finalBody = formData;
|
||||
} else if (binary) {
|
||||
finalBody = binary;
|
||||
} else if (body !== undefined) {
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
finalBody = JSON.stringify(body);
|
||||
}
|
||||
const request = resolveApiRequest({
|
||||
formData,
|
||||
binary,
|
||||
body,
|
||||
headers: finalHeaders,
|
||||
});
|
||||
const response = await fetch(`${API}${pathname}`, {
|
||||
method,
|
||||
headers: finalHeaders,
|
||||
body: finalBody,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
@@ -95,36 +93,8 @@ function gameMetadata(title) {
|
||||
};
|
||||
}
|
||||
|
||||
/** 发布用封面图片 part:字节随 `cover` part 提交,服务端直写项目快照桶。 */
|
||||
function coverImagePart(id) {
|
||||
return {
|
||||
bytes: COVER_PNG,
|
||||
fileName: `sandbox-${id}.png`,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
}
|
||||
|
||||
/** 组装发布请求体:`metadata` 文本 part + 可选 `cover` / 重复 `screenshot` 二进制 part。 */
|
||||
function publishFormData(metadata, { cover, screenshots = [] } = {}) {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
if (cover) {
|
||||
form.append(
|
||||
'cover',
|
||||
new Blob([cover.bytes], { type: cover.contentType }),
|
||||
cover.fileName,
|
||||
);
|
||||
}
|
||||
for (const screenshot of screenshots) {
|
||||
if (!screenshot) continue;
|
||||
form.append(
|
||||
'screenshot',
|
||||
new Blob([screenshot.bytes], { type: screenshot.contentType }),
|
||||
screenshot.fileName,
|
||||
);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
const coverImagePart = (id) =>
|
||||
imagePart('cover', id, { fileName: `sandbox-${id}.png` });
|
||||
|
||||
const PROBE_HELPER = "export const marker = 'helper-ok';\n";
|
||||
|
||||
|
||||
@@ -21,10 +21,12 @@ import path from 'node:path';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const COVER_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
import {
|
||||
imagePart,
|
||||
publishFormData,
|
||||
resolveApiRequest,
|
||||
} from './game-distribution-e2e-helpers.mjs';
|
||||
|
||||
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:4198';
|
||||
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
|
||||
const ADMIN_USER = (process.env.E2E_ADMIN_USER ?? '').trim();
|
||||
@@ -65,20 +67,16 @@ async function api(pathname, options = {}) {
|
||||
} = options;
|
||||
const finalHeaders = { ...ENVELOPE, ...headers };
|
||||
if (token) finalHeaders.Authorization = `Bearer ${token}`;
|
||||
let finalBody;
|
||||
if (formData) {
|
||||
// multipart/form-data:不手动设 Content-Type,交给 fetch 生成 boundary。
|
||||
finalBody = formData;
|
||||
} else if (binary) {
|
||||
finalBody = binary;
|
||||
} else if (body !== undefined) {
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
finalBody = JSON.stringify(body);
|
||||
}
|
||||
const request = resolveApiRequest({
|
||||
formData,
|
||||
binary,
|
||||
body,
|
||||
headers: finalHeaders,
|
||||
});
|
||||
const response = await fetch(`${API}${pathname}`, {
|
||||
method,
|
||||
headers: finalHeaders,
|
||||
body: finalBody,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
@@ -148,36 +146,8 @@ function gameMetadata(title) {
|
||||
};
|
||||
}
|
||||
|
||||
/** 发布用封面图片 part:字节随 `cover` part 提交,服务端直写项目快照桶。 */
|
||||
function coverImagePart(id) {
|
||||
return {
|
||||
bytes: COVER_PNG,
|
||||
fileName: `upload-resume-${id}.png`,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
}
|
||||
|
||||
/** 组装发布请求体:`metadata` 文本 part + 可选 `cover` / 重复 `screenshot` 二进制 part。 */
|
||||
function publishFormData(metadata, { cover, screenshots = [] } = {}) {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
if (cover) {
|
||||
form.append(
|
||||
'cover',
|
||||
new Blob([cover.bytes], { type: cover.contentType }),
|
||||
cover.fileName,
|
||||
);
|
||||
}
|
||||
for (const screenshot of screenshots) {
|
||||
if (!screenshot) continue;
|
||||
form.append(
|
||||
'screenshot',
|
||||
new Blob([screenshot.bytes], { type: screenshot.contentType }),
|
||||
screenshot.fileName,
|
||||
);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
const coverImagePart = (id) =>
|
||||
imagePart('cover', id, { fileName: `upload-resume-${id}.png` });
|
||||
|
||||
// 真实 ZIP(> 8 MiB,随机内容不可压缩),根目录含 index.html,条目数与声明的 packageFileCount 一致。
|
||||
async function buildLargeFixtureZip() {
|
||||
|
||||
@@ -21,10 +21,12 @@ import { createHash } from 'node:crypto';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const COVER_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
import {
|
||||
imagePart,
|
||||
publishFormData,
|
||||
resolveApiRequest,
|
||||
} from './game-distribution-e2e-helpers.mjs';
|
||||
|
||||
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:4198';
|
||||
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
|
||||
const ADMIN_USER = (process.env.E2E_ADMIN_USER ?? '').trim();
|
||||
@@ -59,20 +61,16 @@ async function api(path, options = {}) {
|
||||
} = options;
|
||||
const finalHeaders = { ...ENVELOPE, ...headers };
|
||||
if (token) finalHeaders.Authorization = `Bearer ${token}`;
|
||||
let finalBody;
|
||||
if (formData) {
|
||||
// multipart/form-data:不手动设 Content-Type,交给 fetch 生成 boundary。
|
||||
finalBody = formData;
|
||||
} else if (binary) {
|
||||
finalBody = binary;
|
||||
} else if (body !== undefined) {
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
finalBody = JSON.stringify(body);
|
||||
}
|
||||
const request = resolveApiRequest({
|
||||
formData,
|
||||
binary,
|
||||
body,
|
||||
headers: finalHeaders,
|
||||
});
|
||||
const response = await fetch(`${API}${path}`, {
|
||||
method,
|
||||
headers: finalHeaders,
|
||||
body: finalBody,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
@@ -112,36 +110,8 @@ function gameMetadata(title) {
|
||||
};
|
||||
}
|
||||
|
||||
/** 发布用封面图片 part:字节随 `cover` part 提交,服务端直写项目快照桶。 */
|
||||
function coverImagePart(id) {
|
||||
return {
|
||||
bytes: COVER_PNG,
|
||||
fileName: `upload-safety-${id}.png`,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
}
|
||||
|
||||
/** 组装发布请求体:`metadata` 文本 part + 可选 `cover` / 重复 `screenshot` 二进制 part。 */
|
||||
function publishFormData(metadata, { cover, screenshots = [] } = {}) {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
if (cover) {
|
||||
form.append(
|
||||
'cover',
|
||||
new Blob([cover.bytes], { type: cover.contentType }),
|
||||
cover.fileName,
|
||||
);
|
||||
}
|
||||
for (const screenshot of screenshots) {
|
||||
if (!screenshot) continue;
|
||||
form.append(
|
||||
'screenshot',
|
||||
new Blob([screenshot.bytes], { type: screenshot.contentType }),
|
||||
screenshot.fileName,
|
||||
);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
const coverImagePart = (id) =>
|
||||
imagePart('cover', id, { fileName: `upload-safety-${id}.png` });
|
||||
|
||||
async function main() {
|
||||
const adminLogin = await api('/admin/api/login', {
|
||||
|
||||
@@ -15,10 +15,12 @@ import path from 'node:path';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const COVER_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
import {
|
||||
imagePart,
|
||||
publishFormData,
|
||||
resolveApiRequest,
|
||||
} from './game-distribution-e2e-helpers.mjs';
|
||||
|
||||
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:8082';
|
||||
const API_PORT = Number(new URL(API).port || '80');
|
||||
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
|
||||
@@ -59,20 +61,16 @@ async function api(pathname, options = {}) {
|
||||
} = options;
|
||||
const finalHeaders = { ...ENVELOPE, ...headers };
|
||||
if (token) finalHeaders.Authorization = `Bearer ${token}`;
|
||||
let finalBody;
|
||||
if (formData) {
|
||||
// multipart/form-data:不手动设 Content-Type,交给 fetch 生成 boundary。
|
||||
finalBody = formData;
|
||||
} else if (binary) {
|
||||
finalBody = binary;
|
||||
} else if (body !== undefined) {
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
finalBody = JSON.stringify(body);
|
||||
}
|
||||
const request = resolveApiRequest({
|
||||
formData,
|
||||
binary,
|
||||
body,
|
||||
headers: finalHeaders,
|
||||
});
|
||||
const response = await fetch(`${API}${pathname}`, {
|
||||
method,
|
||||
headers: finalHeaders,
|
||||
body: finalBody,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
@@ -104,36 +102,8 @@ async function adminToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
/** 发布用封面图片 part:字节随 `cover` part 提交,服务端直写项目快照桶。 */
|
||||
function coverImagePart(id) {
|
||||
return {
|
||||
bytes: COVER_PNG,
|
||||
fileName: `restart-${id}.png`,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
}
|
||||
|
||||
/** 组装发布请求体:`metadata` 文本 part + 可选 `cover` / 重复 `screenshot` 二进制 part。 */
|
||||
function publishFormData(metadata, { cover, screenshots = [] } = {}) {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
if (cover) {
|
||||
form.append(
|
||||
'cover',
|
||||
new Blob([cover.bytes], { type: cover.contentType }),
|
||||
cover.fileName,
|
||||
);
|
||||
}
|
||||
for (const screenshot of screenshots) {
|
||||
if (!screenshot) continue;
|
||||
form.append(
|
||||
'screenshot',
|
||||
new Blob([screenshot.bytes], { type: screenshot.contentType }),
|
||||
screenshot.fileName,
|
||||
);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
const coverImagePart = (id) =>
|
||||
imagePart('cover', id, { fileName: `restart-${id}.png` });
|
||||
|
||||
function gameMetadata(title, coverObjectKey) {
|
||||
return {
|
||||
|
||||
@@ -10,10 +10,12 @@ import path from 'node:path';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const COVER_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
import {
|
||||
imagePart,
|
||||
publishFormData,
|
||||
resolveApiRequest,
|
||||
} from './game-distribution-e2e-helpers.mjs';
|
||||
|
||||
const WEB = process.env.E2E_WEB_BASE ?? 'http://127.0.0.1:3000';
|
||||
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:8082';
|
||||
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
|
||||
@@ -45,20 +47,16 @@ async function api(pathname, options = {}) {
|
||||
} = options;
|
||||
const finalHeaders = { ...ENVELOPE, ...headers };
|
||||
if (token) finalHeaders.Authorization = `Bearer ${token}`;
|
||||
let finalBody;
|
||||
if (formData) {
|
||||
// multipart/form-data:不手动设 Content-Type,交给 fetch 生成 boundary。
|
||||
finalBody = formData;
|
||||
} else if (binary) {
|
||||
finalBody = binary;
|
||||
} else if (body !== undefined) {
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
finalBody = JSON.stringify(body);
|
||||
}
|
||||
const request = resolveApiRequest({
|
||||
formData,
|
||||
binary,
|
||||
body,
|
||||
headers: finalHeaders,
|
||||
});
|
||||
const response = await fetch(`${API}${pathname}`, {
|
||||
method,
|
||||
headers: finalHeaders,
|
||||
body: finalBody,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
@@ -77,36 +75,8 @@ async function loadPlaywright() {
|
||||
return requireFromDir('playwright');
|
||||
}
|
||||
|
||||
/** 发布用封面图片 part:字节随 `cover` part 提交,服务端直写项目快照桶。 */
|
||||
function coverImagePart(id) {
|
||||
return {
|
||||
bytes: COVER_PNG,
|
||||
fileName: `a11y-${id}.png`,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
}
|
||||
|
||||
/** 组装发布请求体:`metadata` 文本 part + 可选 `cover` / 重复 `screenshot` 二进制 part。 */
|
||||
function publishFormData(metadata, { cover, screenshots = [] } = {}) {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
if (cover) {
|
||||
form.append(
|
||||
'cover',
|
||||
new Blob([cover.bytes], { type: cover.contentType }),
|
||||
cover.fileName,
|
||||
);
|
||||
}
|
||||
for (const screenshot of screenshots) {
|
||||
if (!screenshot) continue;
|
||||
form.append(
|
||||
'screenshot',
|
||||
new Blob([screenshot.bytes], { type: screenshot.contentType }),
|
||||
screenshot.fileName,
|
||||
);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
const coverImagePart = (id) =>
|
||||
imagePart('cover', id, { fileName: `a11y-${id}.png` });
|
||||
|
||||
async function loginThroughWebUi(page, phone) {
|
||||
await page.goto(`${WEB}/games`, { waitUntil: 'commit', timeout: 120_000 });
|
||||
|
||||
@@ -12,10 +12,12 @@ import path from 'node:path';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const COVER_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
import {
|
||||
imagePart,
|
||||
publishFormData,
|
||||
resolveApiRequest,
|
||||
} from './game-distribution-e2e-helpers.mjs';
|
||||
|
||||
const WEB = process.env.E2E_WEB_BASE ?? 'http://127.0.0.1:3000';
|
||||
const API = process.env.E2E_API_BASE ?? 'http://127.0.0.1:8082';
|
||||
const ENVELOPE = { 'x-genarrative-response-envelope': 'v1' };
|
||||
@@ -79,20 +81,16 @@ async function api(pathname, options = {}) {
|
||||
} = options;
|
||||
const finalHeaders = { ...ENVELOPE, ...headers };
|
||||
if (token) finalHeaders.Authorization = `Bearer ${token}`;
|
||||
let finalBody;
|
||||
if (formData) {
|
||||
// multipart/form-data:不手动设 Content-Type,交给 fetch 生成 boundary。
|
||||
finalBody = formData;
|
||||
} else if (binary) {
|
||||
finalBody = binary;
|
||||
} else if (body !== undefined) {
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
finalBody = JSON.stringify(body);
|
||||
}
|
||||
const request = resolveApiRequest({
|
||||
formData,
|
||||
binary,
|
||||
body,
|
||||
headers: finalHeaders,
|
||||
});
|
||||
const response = await fetch(`${API}${pathname}`, {
|
||||
method,
|
||||
headers: finalHeaders,
|
||||
body: finalBody,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
@@ -110,36 +108,8 @@ async function api(pathname, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/** 发布用封面图片 part:字节随 `cover` part 提交,服务端直写项目快照桶。 */
|
||||
function coverImagePart(id) {
|
||||
return {
|
||||
bytes: COVER_PNG,
|
||||
fileName: `web-e2e-${id}.png`,
|
||||
contentType: 'image/png',
|
||||
};
|
||||
}
|
||||
|
||||
/** 组装发布请求体:`metadata` 文本 part + 可选 `cover` / 重复 `screenshot` 二进制 part。 */
|
||||
function publishFormData(metadata, { cover, screenshots = [] } = {}) {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
if (cover) {
|
||||
form.append(
|
||||
'cover',
|
||||
new Blob([cover.bytes], { type: cover.contentType }),
|
||||
cover.fileName,
|
||||
);
|
||||
}
|
||||
for (const screenshot of screenshots) {
|
||||
if (!screenshot) continue;
|
||||
form.append(
|
||||
'screenshot',
|
||||
new Blob([screenshot.bytes], { type: screenshot.contentType }),
|
||||
screenshot.fileName,
|
||||
);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
const coverImagePart = (id) =>
|
||||
imagePart('cover', id, { fileName: `web-e2e-${id}.png` });
|
||||
|
||||
async function publishFixtureGame(stamp, title) {
|
||||
const adminLogin = await api('/admin/api/login', {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// 游戏分发 E2E 脚本共用的 multipart 发布 helper。
|
||||
//
|
||||
// 这些脚本都直连 api-server,发布口是同一套契约:`metadata` JSON 文本 part + 可选 `cover`
|
||||
// 二进制 part + 按顺序重复的 `screenshot` 二进制 part;各自的 `api()` 也只在 body 形态上分支。
|
||||
// 这里收敛这段稳定契约,脚本只保留自己的路径、鉴权头与返回形状差异。
|
||||
|
||||
/** 1x1 透明 PNG:仅用于满足发布必填的封面/截图 part,内容不参与断言。 */
|
||||
export const COVER_PNG = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64',
|
||||
);
|
||||
|
||||
/**
|
||||
* 发布用图片 part。
|
||||
*
|
||||
* 默认文件名 `<kind>-<id>.png`、`image/png`;脚本可通过 `options.fileName` 保留自己的前缀,
|
||||
* 或用 `options.bytes` / `options.contentType` 覆盖字节与类型。
|
||||
*/
|
||||
export function imagePart(kind, id, options = {}) {
|
||||
const {
|
||||
bytes = COVER_PNG,
|
||||
fileName = `${kind}-${id}.png`,
|
||||
contentType = 'image/png',
|
||||
} = options;
|
||||
return { bytes, fileName, contentType };
|
||||
}
|
||||
|
||||
/**
|
||||
* 封面 part 便捷封装:等价于 `imagePart('cover', id, options)`。
|
||||
*
|
||||
* 脚本可用 `options.fileName` 给出完整文件名,或用 `options.namePrefix` 生成
|
||||
* `<prefix>-<id>.png`。
|
||||
*/
|
||||
export function coverImagePart(id, options = {}) {
|
||||
const { namePrefix, fileName, ...rest } = options;
|
||||
const resolvedFileName =
|
||||
fileName ?? (namePrefix ? `${namePrefix}-${id}.png` : `cover-${id}.png`);
|
||||
return imagePart('cover', id, { ...rest, fileName: resolvedFileName });
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装发布请求体:`metadata` 文本 part + 可选 `cover` / 按顺序重复的 `screenshot` 二进制 part。
|
||||
*
|
||||
* `metadata.screenshots` 里的 `null` 槽位按出现顺序消费 `screenshots` 数组里的 part;string
|
||||
* 槽位沿用该 objectKey。调用方负责保证 null 槽位数量与 part 数量一致(不一致服务端会 400)。
|
||||
*/
|
||||
export function publishFormData(metadata, { cover, screenshots = [] } = {}) {
|
||||
const form = new FormData();
|
||||
form.append('metadata', JSON.stringify(metadata));
|
||||
if (cover) {
|
||||
form.append(
|
||||
'cover',
|
||||
new Blob([cover.bytes], { type: cover.contentType }),
|
||||
cover.fileName,
|
||||
);
|
||||
}
|
||||
for (const screenshot of screenshots) {
|
||||
if (!screenshot) continue;
|
||||
form.append(
|
||||
'screenshot',
|
||||
new Blob([screenshot.bytes], { type: screenshot.contentType }),
|
||||
screenshot.fileName,
|
||||
);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推导 `api()` 的最终请求头与请求体。
|
||||
*
|
||||
* - `formData`:multipart/form-data,不手动设 `Content-Type`,交给 fetch 生成 boundary。
|
||||
* - `binary`:直接作为 body(例如分片 PUT 的字节)。
|
||||
* - `body !== undefined`:按 JSON 序列化,并补 `Content-Type: application/json`。
|
||||
*
|
||||
* 调用方先自行合并 `{ ...ENVELOPE, ...headers }` 与 `Authorization`,再把结果作为 `headers`
|
||||
* 传进来;返回值里的 `headers` 是补完 `Content-Type` 后的副本。
|
||||
*/
|
||||
export function resolveApiRequest({
|
||||
formData,
|
||||
binary,
|
||||
body,
|
||||
headers = {},
|
||||
} = {}) {
|
||||
const finalHeaders = { ...headers };
|
||||
let finalBody;
|
||||
if (formData) {
|
||||
finalBody = formData;
|
||||
} else if (binary) {
|
||||
finalBody = binary;
|
||||
} else if (body !== undefined) {
|
||||
finalHeaders['Content-Type'] = 'application/json';
|
||||
finalBody = JSON.stringify(body);
|
||||
}
|
||||
return { headers: finalHeaders, body: finalBody };
|
||||
}
|
||||
Reference in New Issue
Block a user