From ac36837add26a6facb410dce6fffe0eb565382bb Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 27 Jul 2026 14:42:34 +0800 Subject: [PATCH] test(chat): enforce locked image URL masking --- e2e/fixtures/data/chat.ts | 4 +-- e2e/fixtures/helpers/payment.ts | 12 +++++++- e2e/specs/mock/paid-image.spec.ts | 28 ++++++------------- .../image-unlock-insufficient-credits.spec.ts | 27 +++++------------- .../responses/send-message-photo-paywall.json | 2 +- .../chat/__tests__/chat-helpers.test.ts | 10 +++---- 6 files changed, 34 insertions(+), 49 deletions(-) diff --git a/e2e/fixtures/data/chat.ts b/e2e/fixtures/data/chat.ts index 8303b19b..2d47be83 100644 --- a/e2e/fixtures/data/chat.ts +++ b/e2e/fixtures/data/chat.ts @@ -75,13 +75,13 @@ export const paidImageChatSendResponse = { messageId: paidImageMessageId, isGuest: true, 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 }, }; export function createPaidImageHistoryResponse(unlocked: boolean) { 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, }; } diff --git a/e2e/fixtures/helpers/payment.ts b/e2e/fixtures/helpers/payment.ts index cc813859..83d327fa 100644 --- a/e2e/fixtures/helpers/payment.ts +++ b/e2e/fixtures/helpers/payment.ts @@ -1,9 +1,19 @@ import { expect, type Page } from "@playwright/test"; 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 orderStatusRequestPromise = page.waitForRequest("**/api/payment/order-status**"); - await page.getByRole("button", { name: /Pay and Activ/i }).click(); + await checkoutButton.click(); const createOrderRequest = await createOrderRequestPromise; expect(createOrderRequest.postDataJSON()).toMatchObject({ planId: "vip_monthly", payChannel: "stripe" }); await orderStatusRequestPromise; diff --git a/e2e/specs/mock/paid-image.spec.ts b/e2e/specs/mock/paid-image.spec.ts index 2a4bdadc..1c089372 100644 --- a/e2e/specs/mock/paid-image.spec.ts +++ b/e2e/specs/mock/paid-image.spec.ts @@ -19,10 +19,6 @@ import { switchToEmailSignIn, } from "@e2e/fixtures/test-helpers"; -const paidImageOverlayUrl = new RegExp( - `${defaultCharacterChatPath}\\?image=${encodeURIComponent(paidImageDisplayMessageId)}$`, -); - test.beforeEach(async ({ baseURL, context, page }) => { await clearBrowserState(context, page, baseURL); await mockCoreApis(page, { @@ -48,26 +44,18 @@ test("guest can unlock a paid image after email login and subscription payment", message: "给我发图片", }); - const paidImageButton = page.getByRole("button", { - name: "Open image in fullscreen", + const lockedImageCard = page.getByRole("group", { + name: "Locked private image", }).last(); - await expect(paidImageButton).toBeVisible(); - - await paidImageButton.click(); - await expect(page).toHaveURL(paidImageOverlayUrl); - - const lockedImageDialog = page.getByRole("dialog", { - name: "Locked fullscreen image", - }); - await expect(lockedImageDialog).toBeVisible(); - - await lockedImageDialog - .getByRole("button", { name: "Unlock high-definition large image" }) + await expect(lockedImageCard).toBeVisible(); + await expect(lockedImageCard.locator("img")).toHaveCount(0); + await lockedImageCard + .getByRole("button", { name: "Unlock private image" }) .click(); await expect(page).toHaveURL(/\/auth\?redirect=/); expect(new URL(page.url()).searchParams.get("redirect")).toBe( - `${defaultCharacterChatPath}?image=${encodeURIComponent(paidImageDisplayMessageId)}`, + defaultCharacterChatPath, ); await switchToEmailSignIn(page); @@ -75,7 +63,7 @@ test("guest can unlock a paid image after email login and subscription payment", "**/api/chat/unlock-private", ); await submitEmailLogin(page, { - expectedUrl: paidImageOverlayUrl, + expectedUrl: new RegExp(`${defaultCharacterChatPath}$`), }); const unlockRequest = await unlockRequestPromise; diff --git a/e2e/specs/mock/unlock-message/image-unlock-insufficient-credits.spec.ts b/e2e/specs/mock/unlock-message/image-unlock-insufficient-credits.spec.ts index b6f05802..fdc24a33 100644 --- a/e2e/specs/mock/unlock-message/image-unlock-insufficient-credits.spec.ts +++ b/e2e/specs/mock/unlock-message/image-unlock-insufficient-credits.spec.ts @@ -2,22 +2,16 @@ import { expect, test } from "@playwright/test"; import { mockCoreApis } from "@e2e/fixtures/api-mocks"; import { - paidImageDisplayMessageId, paidImageMessageId, } from "@e2e/fixtures/test-data"; import { clearBrowserState, - defaultCharacterChatPath, dismissChatInterruptions, expectInsufficientCreditsDialog, expectVipChatSubscriptionUrl, signInWithEmailAndOpenChat, } from "@e2e/fixtures/test-helpers"; -const paidImageOverlayUrl = new RegExp( - `${defaultCharacterChatPath}\\?image=${encodeURIComponent(paidImageDisplayMessageId)}$`, -); - test.beforeEach(async ({ baseURL, context, page }) => { await clearBrowserState(context, page, baseURL); 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, }) => { await signInWithEmailAndOpenChat(page); await dismissChatInterruptions(page); - const paidImageButton = page.getByRole("button", { - name: "Open image in fullscreen", + const lockedImageCard = page.getByRole("group", { + name: "Locked private image", }).last(); - await expect(paidImageButton).toBeVisible({ timeout: 10_000 }); - - await paidImageButton.click(); - await expect(page).toHaveURL(paidImageOverlayUrl); - - const lockedImageDialog = page.getByRole("dialog", { - name: "Locked fullscreen image", - }); - await expect(lockedImageDialog).toBeVisible(); + await expect(lockedImageCard).toBeVisible({ timeout: 10_000 }); + await expect(lockedImageCard.locator("img")).toHaveCount(0); const unlockRequestPromise = page.waitForRequest( "**/api/chat/unlock-private", ); - await lockedImageDialog - .getByRole("button", { name: "Unlock high-definition large image" }) + await lockedImageCard + .getByRole("button", { name: "Unlock private image" }) .click(); const unlockRequest = await unlockRequestPromise; diff --git a/src/data/mock/chat/responses/send-message-photo-paywall.json b/src/data/mock/chat/responses/send-message-photo-paywall.json index 0a8b25a8..7fb0d3dc 100644 --- a/src/data/mock/chat/responses/send-message-photo-paywall.json +++ b/src/data/mock/chat/responses/send-message-photo-paywall.json @@ -10,7 +10,7 @@ "timestamp": 1782180725000, "image": { "type": "elio_schedule", - "url": "https://cdn.cozsweet.com/mock/chat/elio-schedule-locked.jpg" + "url": null }, "lockDetail": { "locked": true, diff --git a/src/stores/chat/__tests__/chat-helpers.test.ts b/src/stores/chat/__tests__/chat-helpers.test.ts index 10b908cc..0e62e007 100644 --- a/src/stores/chat/__tests__/chat-helpers.test.ts +++ b/src/stores/chat/__tests__/chat-helpers.test.ts @@ -185,12 +185,12 @@ describe("sendResponseToUiMessage", () => { 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( makeResponse({ image: { type: "jpg", - url: "https://example.com/private-photo.jpg", + url: null, }, lockDetail: { locked: true, @@ -199,9 +199,9 @@ describe("sendResponseToUiMessage", () => { }), ); - expect(message.content).toBe(""); - expect(message.imageUrl).toBe("https://example.com/private-photo.jpg"); - expect(message.imagePaywalled).toBe(true); + expect(message.content).toBe("AI reply"); + expect(message.imageUrl).toBeUndefined(); + expect(message.imagePaywalled).toBeUndefined(); expect(message.locked).toBe(true); expect(message.lockReason).toBe("image"); expect(message.lockedPrivate).toBeUndefined();