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