test(chat): enforce locked image URL masking
Docker Image / Build and Push Docker Image (push) Successful in 2m15s
Docker Image / Build and Push Docker Image (push) Successful in 2m15s
This commit is contained in:
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user