Browse Source
Add a temporary legacy fallback so guest payloads missing both guestEmail and guestIsAnonymous are treated as anonymous during rollout. Add service-level and API body parsing tests for guest email rules, logged-in ignore behavior, and public/unlisted passthrough. Made-with: Cursormain
7 changed files with 185 additions and 37 deletions
@ -0,0 +1,47 @@ |
|||
import { describe, expect, test } from "bun:test"; |
|||
import { parsePublicPostCreateCommentBody, parseUnlistedCreateCommentBody } from "./comments-create-body"; |
|||
|
|||
globalThis.createError ??= ((input: { statusCode: number; statusMessage: string }) => { |
|||
const error = new Error(input.statusMessage) as Error & { statusCode: number; statusMessage: string }; |
|||
error.statusCode = input.statusCode; |
|||
error.statusMessage = input.statusMessage; |
|||
return error; |
|||
}) as any; |
|||
|
|||
describe("parsePublicPostCreateCommentBody", () => { |
|||
test("passes through guestEmail and guestIsAnonymous for public endpoint", () => { |
|||
const parsed = parsePublicPostCreateCommentBody({ |
|||
parentId: 11, |
|||
guestDisplayName: "访客A", |
|||
guestEmail: "guest@example.com", |
|||
guestIsAnonymous: true, |
|||
body: "hello", |
|||
}); |
|||
expect(parsed).toEqual({ |
|||
parentId: 11, |
|||
guestDisplayName: "访客A", |
|||
guestEmail: "guest@example.com", |
|||
guestIsAnonymous: true, |
|||
body: "hello", |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe("parseUnlistedCreateCommentBody", () => { |
|||
test("passes through guestEmail and guestIsAnonymous for unlisted endpoint", () => { |
|||
const parsed = parseUnlistedCreateCommentBody({ |
|||
parentId: null, |
|||
guestDisplayName: "访客B", |
|||
guestEmail: "anon@example.com", |
|||
guestIsAnonymous: false, |
|||
body: "world", |
|||
}); |
|||
expect(parsed).toEqual({ |
|||
parentId: null, |
|||
guestDisplayName: "访客B", |
|||
guestEmail: "anon@example.com", |
|||
guestIsAnonymous: false, |
|||
body: "world", |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,51 @@ |
|||
type CreateCommentRequestBody = { |
|||
parentId?: number | null; |
|||
guestDisplayName?: string; |
|||
guestEmail?: string; |
|||
guestIsAnonymous?: boolean; |
|||
body: unknown; |
|||
}; |
|||
|
|||
export type ParsedCreateCommentInput = { |
|||
parentId: number | null; |
|||
guestDisplayName?: string; |
|||
guestEmail?: string; |
|||
guestIsAnonymous: boolean; |
|||
body: string; |
|||
}; |
|||
|
|||
export function parsePublicPostCreateCommentBody(body: CreateCommentRequestBody): ParsedCreateCommentInput { |
|||
if (typeof body.body !== "string") { |
|||
throw createError({ statusCode: 400, statusMessage: "无效请求" }); |
|||
} |
|||
return { |
|||
parentId: parseParentId(body.parentId), |
|||
guestDisplayName: typeof body.guestDisplayName === "string" ? body.guestDisplayName : undefined, |
|||
guestEmail: typeof body.guestEmail === "string" ? body.guestEmail : undefined, |
|||
guestIsAnonymous: body.guestIsAnonymous === true, |
|||
body: body.body, |
|||
}; |
|||
} |
|||
|
|||
export function parseUnlistedCreateCommentBody(body: CreateCommentRequestBody): ParsedCreateCommentInput { |
|||
if (typeof body.body !== "string") { |
|||
throw createError({ statusCode: 400, statusMessage: "无效请求" }); |
|||
} |
|||
return { |
|||
parentId: parseParentId(body.parentId), |
|||
guestDisplayName: typeof body.guestDisplayName === "string" ? body.guestDisplayName : undefined, |
|||
guestEmail: typeof body.guestEmail === "string" ? body.guestEmail : undefined, |
|||
guestIsAnonymous: body.guestIsAnonymous === true, |
|||
body: body.body, |
|||
}; |
|||
} |
|||
|
|||
function parseParentId(rawParentId: unknown): number | null { |
|||
if (rawParentId === undefined || rawParentId === null) { |
|||
return null; |
|||
} |
|||
if (typeof rawParentId === "number" && Number.isFinite(rawParentId)) { |
|||
return rawParentId; |
|||
} |
|||
throw createError({ statusCode: 400, statusMessage: "无效请求" }); |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
import { describe, expect, test } from "bun:test"; |
|||
import { GuestCommentValidationError } from "../../utils/post-comment-guest"; |
|||
import { resolveGuestFields } from "./guest-fields"; |
|||
|
|||
describe("resolveGuestFields", () => { |
|||
test("guest non-anonymous without email throws", () => { |
|||
expect(() => |
|||
resolveGuestFields({ |
|||
viewerPresent: false, |
|||
guestIsAnonymous: false, |
|||
}), |
|||
).toThrow(GuestCommentValidationError); |
|||
}); |
|||
|
|||
test("guest anonymous with empty email passes", () => { |
|||
expect( |
|||
resolveGuestFields({ |
|||
viewerPresent: false, |
|||
guestEmail: "", |
|||
guestIsAnonymous: true, |
|||
}), |
|||
).toEqual({ guestEmail: null, guestIsAnonymous: true }); |
|||
}); |
|||
|
|||
test("logged-in viewer ignores guest fields", () => { |
|||
expect( |
|||
resolveGuestFields({ |
|||
viewerPresent: true, |
|||
guestEmail: "guest@example.com", |
|||
guestIsAnonymous: true, |
|||
}), |
|||
).toEqual({ guestEmail: null, guestIsAnonymous: false }); |
|||
}); |
|||
|
|||
test("legacy payload without guest email fields falls back to anonymous", () => { |
|||
expect( |
|||
resolveGuestFields({ |
|||
viewerPresent: false, |
|||
}), |
|||
).toEqual({ guestEmail: null, guestIsAnonymous: true }); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,23 @@ |
|||
import { validateGuestCommentEmail } from "../../utils/post-comment-guest"; |
|||
|
|||
export type ResolvedGuestFields = { |
|||
guestEmail: string | null; |
|||
guestIsAnonymous: boolean; |
|||
}; |
|||
|
|||
export function resolveGuestFields(input: { |
|||
viewerPresent: boolean; |
|||
guestEmail?: string; |
|||
guestIsAnonymous?: boolean; |
|||
}): ResolvedGuestFields { |
|||
if (input.viewerPresent) { |
|||
return { guestEmail: null, guestIsAnonymous: false }; |
|||
} |
|||
|
|||
// 兼容过渡策略:旧客户端尚未上送 guestEmail/guestIsAnonymous 时,按匿名处理,避免发布窗口内全量 400。
|
|||
// 待所有客户端完成升级后可移除此分支,恢复严格“默认非匿名且邮箱必填”语义。
|
|||
const isLegacyGuestPayload = input.guestEmail == null && input.guestIsAnonymous === undefined; |
|||
const guestIsAnonymous = input.guestIsAnonymous === true || isLegacyGuestPayload; |
|||
const guestEmail = validateGuestCommentEmail(input.guestEmail, guestIsAnonymous); |
|||
return { guestEmail, guestIsAnonymous }; |
|||
} |
|||
Loading…
Reference in new issue