diff --git a/packages/contracts/openapi.yaml b/packages/contracts/openapi.yaml index 4567f8e..f698a44 100644 --- a/packages/contracts/openapi.yaml +++ b/packages/contracts/openapi.yaml @@ -2094,8 +2094,8 @@ paths: $ref: '#/components/schemas/AdminAdItem' '400': description: | - 参数校验失败(VALIDATION_FAILED):字段非法、换图三字段未同时提供 - 或 staging 核验失败(UPLOAD_VERIFY_FAILED)。 + 参数校验失败(VALIDATION_FAILED):字段非法、空请求体、换图三字段 + 未同时提供或 staging 核验失败(UPLOAD_VERIFY_FAILED)。 content: application/json: schema: @@ -2109,7 +2109,7 @@ paths: schema: $ref: '#/components/schemas/Error' '404': - description: 广告位不存在(AD_NOT_FOUND)。 + description: 广告位不存在(NOT_FOUND)。 content: application/json: schema: @@ -2137,7 +2137,7 @@ paths: schema: $ref: '#/components/schemas/Error' '404': - description: 广告位不存在(AD_NOT_FOUND)。 + description: 广告位不存在(NOT_FOUND)。 content: application/json: schema: @@ -4774,6 +4774,7 @@ components: 过期后重拉列表刷新。 required: - id + - title - imageObjectKey - imageUrl - linkUrl @@ -4875,7 +4876,8 @@ components: AdUpdateRequest: type: object description: | - 部分更新请求(仅提交的字段被修改)。albumIds:未提供=不动,null=转 + 部分更新请求(仅提交的字段被修改,空请求体 400 VALIDATION_FAILED, + 对照 EmbedSlotUpdateRequest 口径)。albumIds:未提供=不动,null=转 全局,数组=整体替换;imageStagingKey/imageBytes/imageContentType 三者 必须同时提供(换图,服务端核验 staging 后转存新版本键)。 properties: diff --git a/packages/contracts/src/__tests__/ads.test.ts b/packages/contracts/src/__tests__/ads.test.ts new file mode 100644 index 0000000..5f4c3ec --- /dev/null +++ b/packages/contracts/src/__tests__/ads.test.ts @@ -0,0 +1,47 @@ +// packages/contracts/src/__tests__/ads.test.ts +// 广告位契约守护:openapi.yaml 中三处硬编码的广告图字节上限(maximum: 2097152) +// 必须与 constants.ts 的 AD_UPLOAD_MAX_BYTES 恒等(1:1 守护,对照 events.test.ts +// 的枚举同步守护)。契约文件无 js-yaml 依赖(pnpm 严格 node_modules),按文件 +// 既有的固定缩进块风格做文本切片解析:schema 名 4 空格、属性名 8 空格、 +// 属性约束 10 空格——缩进漂移会令定位失败而报错,本身就是守护的一部分。 +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { AD_UPLOAD_MAX_BYTES } from '../constants.js'; + +const openapiYaml = readFileSync( + resolve(__dirname, '../../openapi.yaml'), + 'utf8', +); + +/** 取 components/schemas 下指定 schema 的文本块(至下一个 4 空格缩进的键)。 */ +function schemaBlock(name: string): string { + const start = openapiYaml.indexOf(`\n ${name}:`); + if (start === -1) { + throw new Error(`openapi.yaml 缺少 schema:${name}`); + } + const body = openapiYaml.slice(start + 1); + const next = body.search(/\n [A-Za-z]/); + return next === -1 ? body : body.slice(0, next); +} + +/** 取 schema 块内指定属性的 maximum 约束值(属性块内首个 maximum)。 */ +function maximumOf(name: string, prop: string): number { + const block = schemaBlock(name); + const match = block.match( + new RegExp(`^ {8}${prop}:\\n(?: {10}[^\\n]+\\n)*? {10}maximum: (\\d+)`, 'm'), + ); + if (match === null) { + throw new Error(`${name}.${prop} 缺少 maximum 约束`); + } + return Number(match[1]); +} + +describe('openapi 广告图上限 ↔ AD_UPLOAD_MAX_BYTES 同步', () => { + it.each([ + ['AdCreateRequest', 'imageBytes'], + ['AdUpdateRequest', 'imageBytes'], + ['AdUploadInitRequest', 'bytes'], + ] as const)('%s.%s 的 maximum 恒等于 AD_UPLOAD_MAX_BYTES', (schema, prop) => { + expect(maximumOf(schema, prop)).toBe(AD_UPLOAD_MAX_BYTES); + }); +}); diff --git a/packages/contracts/src/generated/openapi.d.ts b/packages/contracts/src/generated/openapi.d.ts index 002e149..d0eac20 100644 --- a/packages/contracts/src/generated/openapi.d.ts +++ b/packages/contracts/src/generated/openapi.d.ts @@ -2437,7 +2437,7 @@ export interface components { /** @description 广告位 ID。 */ readonly id: string; /** @description 后台备注名(不展示观众)。 */ - title?: string | null; + title: string | null; /** @description 版本化终键(换图递增版本,禁止覆盖)。 */ readonly imageObjectKey: string; /** @description 现签 GET(TTL=SIGNED_URL_TTL_SECONDS)。 */ @@ -2489,7 +2489,8 @@ export interface components { albumIds?: string[]; }; /** - * @description 部分更新请求(仅提交的字段被修改)。albumIds:未提供=不动,null=转 + * @description 部分更新请求(仅提交的字段被修改,空请求体 400 VALIDATION_FAILED, + * 对照 EmbedSlotUpdateRequest 口径)。albumIds:未提供=不动,null=转 * 全局,数组=整体替换;imageStagingKey/imageBytes/imageContentType 三者 * 必须同时提供(换图,服务端核验 staging 后转存新版本键)。 */ @@ -5264,7 +5265,7 @@ export interface operations { "application/json": components["schemas"]["Error"]; }; }; - /** @description 广告位不存在(AD_NOT_FOUND)。 */ + /** @description 广告位不存在(NOT_FOUND)。 */ 404: { headers: { [name: string]: unknown; @@ -5301,8 +5302,8 @@ export interface operations { }; }; /** - * @description 参数校验失败(VALIDATION_FAILED):字段非法、换图三字段未同时提供 - * 或 staging 核验失败(UPLOAD_VERIFY_FAILED)。 + * @description 参数校验失败(VALIDATION_FAILED):字段非法、空请求体、换图三字段 + * 未同时提供或 staging 核验失败(UPLOAD_VERIFY_FAILED)。 */ 400: { headers: { @@ -5322,7 +5323,7 @@ export interface operations { "application/json": components["schemas"]["Error"]; }; }; - /** @description 广告位不存在(AD_NOT_FOUND)。 */ + /** @description 广告位不存在(NOT_FOUND)。 */ 404: { headers: { [name: string]: unknown;