test(chat): enforce locked image URL masking

This commit is contained in:
Codex
2026-07-27 14:42:34 +08:00
parent fe9d31146b
commit ac36837add
6 changed files with 34 additions and 49 deletions
+2 -2
View File
@@ -75,13 +75,13 @@ export const paidImageChatSendResponse = {
messageId: paidImageMessageId, messageId: paidImageMessageId,
isGuest: true, isGuest: true,
timestamp: 1_782_180_725_000, timestamp: 1_782_180_725_000,
image: { type: "elio_schedule", url: paidImageUrl }, image: { type: "elio_schedule", url: null },
lockDetail: { locked: true, showContent: true, showUpgrade: true, reason: "image", hint: "Activate VIP to unlock the full photo.", detail: null }, lockDetail: { locked: true, showContent: true, showUpgrade: true, reason: "image", hint: "Activate VIP to unlock the full photo.", detail: null },
}; };
export function createPaidImageHistoryResponse(unlocked: boolean) { export function createPaidImageHistoryResponse(unlocked: boolean) {
return { return {
messages: [{ role: "assistant", type: "image", content: unlocked ? "" : paidImageChatSendResponse.reply, id: paidImageMessageId, created_at: "2026-06-30T00:00:00.000Z", audioUrl: null, image: { type: "elio_schedule", url: paidImageUrl }, lockDetail: unlocked ? { locked: false, showContent: true, showUpgrade: false, reason: null, hint: null, detail: null } : paidImageChatSendResponse.lockDetail }], messages: [{ role: "assistant", type: "image", content: unlocked ? "" : paidImageChatSendResponse.reply, id: paidImageMessageId, created_at: "2026-06-30T00:00:00.000Z", audioUrl: null, image: { type: "elio_schedule", url: unlocked ? paidImageUrl : null }, lockDetail: unlocked ? { locked: false, showContent: true, showUpgrade: false, reason: null, hint: null, detail: null } : paidImageChatSendResponse.lockDetail }],
total: 1, limit: 50, offset: 0, isVip: unlocked, privateFreeLimit: 0, privateUsedToday: 0, privateCanViewFree: false, total: 1, limit: 50, offset: 0, isVip: unlocked, privateFreeLimit: 0, privateUsedToday: 0, privateCanViewFree: false,
}; };
} }
+11 -1
View File
@@ -1,9 +1,19 @@
import { expect, type Page } from "@playwright/test"; import { expect, type Page } from "@playwright/test";
export async function completeVipPayment(page: Page) { export async function completeVipPayment(page: Page) {
const checkoutButton = page.getByRole("button", { name: "Pay and Top Up" });
await expect(checkoutButton).toBeDisabled();
await page.getByRole("button", { name: /Monthly,/i }).click();
const renewalDialog = page.getByRole("dialog", {
name: "Automatic Renewal Confirmation",
});
await expect(renewalDialog).toBeVisible();
await renewalDialog.getByRole("button", { name: "Confirm" }).click();
await expect(checkoutButton).toBeEnabled();
const createOrderRequestPromise = page.waitForRequest("**/api/payment/create-order"); const createOrderRequestPromise = page.waitForRequest("**/api/payment/create-order");
const orderStatusRequestPromise = page.waitForRequest("**/api/payment/order-status**"); const orderStatusRequestPromise = page.waitForRequest("**/api/payment/order-status**");
await page.getByRole("button", { name: /Pay and Activ/i }).click(); await checkoutButton.click();
const createOrderRequest = await createOrderRequestPromise; const createOrderRequest = await createOrderRequestPromise;
expect(createOrderRequest.postDataJSON()).toMatchObject({ planId: "vip_monthly", payChannel: "stripe" }); expect(createOrderRequest.postDataJSON()).toMatchObject({ planId: "vip_monthly", payChannel: "stripe" });
await orderStatusRequestPromise; await orderStatusRequestPromise;
+8 -20
View File
@@ -19,10 +19,6 @@ import {
switchToEmailSignIn, switchToEmailSignIn,
} from "@e2e/fixtures/test-helpers"; } from "@e2e/fixtures/test-helpers";
const paidImageOverlayUrl = new RegExp(
`${defaultCharacterChatPath}\\?image=${encodeURIComponent(paidImageDisplayMessageId)}$`,
);
test.beforeEach(async ({ baseURL, context, page }) => { test.beforeEach(async ({ baseURL, context, page }) => {
await clearBrowserState(context, page, baseURL); await clearBrowserState(context, page, baseURL);
await mockCoreApis(page, { await mockCoreApis(page, {
@@ -48,26 +44,18 @@ test("guest can unlock a paid image after email login and subscription payment",
message: "给我发图片", message: "给我发图片",
}); });
const paidImageButton = page.getByRole("button", { const lockedImageCard = page.getByRole("group", {
name: "Open image in fullscreen", name: "Locked private image",
}).last(); }).last();
await expect(paidImageButton).toBeVisible(); await expect(lockedImageCard).toBeVisible();
await expect(lockedImageCard.locator("img")).toHaveCount(0);
await paidImageButton.click(); await lockedImageCard
await expect(page).toHaveURL(paidImageOverlayUrl); .getByRole("button", { name: "Unlock private image" })
const lockedImageDialog = page.getByRole("dialog", {
name: "Locked fullscreen image",
});
await expect(lockedImageDialog).toBeVisible();
await lockedImageDialog
.getByRole("button", { name: "Unlock high-definition large image" })
.click(); .click();
await expect(page).toHaveURL(/\/auth\?redirect=/); await expect(page).toHaveURL(/\/auth\?redirect=/);
expect(new URL(page.url()).searchParams.get("redirect")).toBe( expect(new URL(page.url()).searchParams.get("redirect")).toBe(
`${defaultCharacterChatPath}?image=${encodeURIComponent(paidImageDisplayMessageId)}`, defaultCharacterChatPath,
); );
await switchToEmailSignIn(page); await switchToEmailSignIn(page);
@@ -75,7 +63,7 @@ test("guest can unlock a paid image after email login and subscription payment",
"**/api/chat/unlock-private", "**/api/chat/unlock-private",
); );
await submitEmailLogin(page, { await submitEmailLogin(page, {
expectedUrl: paidImageOverlayUrl, expectedUrl: new RegExp(`${defaultCharacterChatPath}$`),
}); });
const unlockRequest = await unlockRequestPromise; const unlockRequest = await unlockRequestPromise;
@@ -2,22 +2,16 @@ import { expect, test } from "@playwright/test";
import { mockCoreApis } from "@e2e/fixtures/api-mocks"; import { mockCoreApis } from "@e2e/fixtures/api-mocks";
import { import {
paidImageDisplayMessageId,
paidImageMessageId, paidImageMessageId,
} from "@e2e/fixtures/test-data"; } from "@e2e/fixtures/test-data";
import { import {
clearBrowserState, clearBrowserState,
defaultCharacterChatPath,
dismissChatInterruptions, dismissChatInterruptions,
expectInsufficientCreditsDialog, expectInsufficientCreditsDialog,
expectVipChatSubscriptionUrl, expectVipChatSubscriptionUrl,
signInWithEmailAndOpenChat, signInWithEmailAndOpenChat,
} from "@e2e/fixtures/test-helpers"; } from "@e2e/fixtures/test-helpers";
const paidImageOverlayUrl = new RegExp(
`${defaultCharacterChatPath}\\?image=${encodeURIComponent(paidImageDisplayMessageId)}$`,
);
test.beforeEach(async ({ baseURL, context, page }) => { test.beforeEach(async ({ baseURL, context, page }) => {
await clearBrowserState(context, page, baseURL); await clearBrowserState(context, page, baseURL);
await mockCoreApis(page, { await mockCoreApis(page, {
@@ -25,30 +19,23 @@ test.beforeEach(async ({ baseURL, context, page }) => {
}); });
}); });
test("user sees insufficient credits when unlocking a paid image from fullscreen and can navigate to subscription", async ({ test("user sees insufficient credits when unlocking from the locked image card and can navigate to subscription", async ({
page, page,
}) => { }) => {
await signInWithEmailAndOpenChat(page); await signInWithEmailAndOpenChat(page);
await dismissChatInterruptions(page); await dismissChatInterruptions(page);
const paidImageButton = page.getByRole("button", { const lockedImageCard = page.getByRole("group", {
name: "Open image in fullscreen", name: "Locked private image",
}).last(); }).last();
await expect(paidImageButton).toBeVisible({ timeout: 10_000 }); await expect(lockedImageCard).toBeVisible({ timeout: 10_000 });
await expect(lockedImageCard.locator("img")).toHaveCount(0);
await paidImageButton.click();
await expect(page).toHaveURL(paidImageOverlayUrl);
const lockedImageDialog = page.getByRole("dialog", {
name: "Locked fullscreen image",
});
await expect(lockedImageDialog).toBeVisible();
const unlockRequestPromise = page.waitForRequest( const unlockRequestPromise = page.waitForRequest(
"**/api/chat/unlock-private", "**/api/chat/unlock-private",
); );
await lockedImageDialog await lockedImageCard
.getByRole("button", { name: "Unlock high-definition large image" }) .getByRole("button", { name: "Unlock private image" })
.click(); .click();
const unlockRequest = await unlockRequestPromise; const unlockRequest = await unlockRequestPromise;
@@ -10,7 +10,7 @@
"timestamp": 1782180725000, "timestamp": 1782180725000,
"image": { "image": {
"type": "elio_schedule", "type": "elio_schedule",
"url": "https://cdn.cozsweet.com/mock/chat/elio-schedule-locked.jpg" "url": null
}, },
"lockDetail": { "lockDetail": {
"locked": true, "locked": true,
@@ -185,12 +185,12 @@ describe("sendResponseToUiMessage", () => {
expect(message.privateMessageHint).toBeUndefined(); expect(message.privateMessageHint).toBeUndefined();
}); });
it("keeps paywalled image messages visible but locked", () => { it("keeps image messages locked without retaining the protected URL", () => {
const message = sendResponseToUiMessage( const message = sendResponseToUiMessage(
makeResponse({ makeResponse({
image: { image: {
type: "jpg", type: "jpg",
url: "https://example.com/private-photo.jpg", url: null,
}, },
lockDetail: { lockDetail: {
locked: true, locked: true,
@@ -199,9 +199,9 @@ describe("sendResponseToUiMessage", () => {
}), }),
); );
expect(message.content).toBe(""); expect(message.content).toBe("AI reply");
expect(message.imageUrl).toBe("https://example.com/private-photo.jpg"); expect(message.imageUrl).toBeUndefined();
expect(message.imagePaywalled).toBe(true); expect(message.imagePaywalled).toBeUndefined();
expect(message.locked).toBe(true); expect(message.locked).toBe(true);
expect(message.lockReason).toBe("image"); expect(message.lockReason).toBe("image");
expect(message.lockedPrivate).toBeUndefined(); expect(message.lockedPrivate).toBeUndefined();