test(auth): cover PSID external entry login
This commit is contained in:
@@ -11,6 +11,7 @@ export interface MockCoreApisOptions {
|
|||||||
paidImageFlow?: boolean;
|
paidImageFlow?: boolean;
|
||||||
paidImageInsufficientCreditsFlow?: boolean;
|
paidImageInsufficientCreditsFlow?: boolean;
|
||||||
paidVoiceInsufficientCreditsFlow?: boolean;
|
paidVoiceInsufficientCreditsFlow?: boolean;
|
||||||
|
psidLoginFlow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mockCoreApis(page: Page, options: MockCoreApisOptions = {}) {
|
export async function mockCoreApis(page: Page, options: MockCoreApisOptions = {}) {
|
||||||
@@ -20,12 +21,14 @@ export async function mockCoreApis(page: Page, options: MockCoreApisOptions = {}
|
|||||||
paidImageFlow: options.paidImageFlow ?? false,
|
paidImageFlow: options.paidImageFlow ?? false,
|
||||||
paidImageInsufficientCreditsFlow: options.paidImageInsufficientCreditsFlow ?? false,
|
paidImageInsufficientCreditsFlow: options.paidImageInsufficientCreditsFlow ?? false,
|
||||||
paidVoiceInsufficientCreditsFlow: options.paidVoiceInsufficientCreditsFlow ?? false,
|
paidVoiceInsufficientCreditsFlow: options.paidVoiceInsufficientCreditsFlow ?? false,
|
||||||
|
psidLoginFlow: options.psidLoginFlow ?? false,
|
||||||
};
|
};
|
||||||
const chatState = { hasExpiredChatSend: false, paidImageRequested: false, paidImageUnlocked: false };
|
const chatState = { hasExpiredChatSend: false, paidImageRequested: false, paidImageUnlocked: false };
|
||||||
|
|
||||||
await registerAuthMocks(page, {
|
await registerAuthMocks(page, {
|
||||||
chatSendTokenRefreshFlow: chatOptions.chatSendTokenRefreshFlow,
|
chatSendTokenRefreshFlow: chatOptions.chatSendTokenRefreshFlow,
|
||||||
isChatSendTokenExpired: () => chatState.hasExpiredChatSend,
|
isChatSendTokenExpired: () => chatState.hasExpiredChatSend,
|
||||||
|
psidLoginFlow: chatOptions.psidLoginFlow,
|
||||||
});
|
});
|
||||||
await registerUserMocks(page);
|
await registerUserMocks(page);
|
||||||
await registerChatMocks(page, chatOptions, chatState);
|
await registerChatMocks(page, chatOptions, chatState);
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
import type { Page } from "@playwright/test";
|
import type { Page } from "@playwright/test";
|
||||||
|
|
||||||
import { apiEnvelope } from "../data/common";
|
import { apiEnvelope } from "../data/common";
|
||||||
import { emailLoginResponse, guestLoginResponse, refreshedEmailLoginResponse, refreshedGuestLoginResponse } from "../data/auth";
|
import { emailLoginResponse, guestLoginResponse, psidLoginResponse, refreshedEmailLoginResponse, refreshedGuestLoginResponse } from "../data/auth";
|
||||||
|
|
||||||
export interface AuthMockState {
|
export interface AuthMockState {
|
||||||
chatSendTokenRefreshFlow: boolean;
|
chatSendTokenRefreshFlow: boolean;
|
||||||
isChatSendTokenExpired: () => boolean;
|
isChatSendTokenExpired: () => boolean;
|
||||||
|
psidLoginFlow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registerAuthMocks(page: Page, state: AuthMockState) {
|
export async function registerAuthMocks(page: Page, state: AuthMockState) {
|
||||||
|
await page.route("**/api/auth/login/facebook/psid", async (route) => {
|
||||||
|
if (!state.psidLoginFlow) {
|
||||||
|
await route.continue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await route.fulfill({ json: apiEnvelope(psidLoginResponse) });
|
||||||
|
});
|
||||||
|
|
||||||
await page.route("**/api/auth/guest", async (route) => {
|
await page.route("**/api/auth/guest", async (route) => {
|
||||||
const response = state.chatSendTokenRefreshFlow && state.isChatSendTokenExpired() ? refreshedGuestLoginResponse : guestLoginResponse;
|
const response = state.chatSendTokenRefreshFlow && state.isChatSendTokenExpired() ? refreshedGuestLoginResponse : guestLoginResponse;
|
||||||
await route.fulfill({ json: apiEnvelope(response) });
|
await route.fulfill({ json: apiEnvelope(response) });
|
||||||
|
|||||||
@@ -49,3 +49,15 @@ export const refreshedEmailLoginResponse = {
|
|||||||
token: "e2e-refreshed-email-token",
|
token: "e2e-refreshed-email-token",
|
||||||
refreshToken: "e2e-refreshed-email-refresh-token",
|
refreshToken: "e2e-refreshed-email-refresh-token",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const psidLoginResponse = {
|
||||||
|
token: "e2e-psid-guest-token",
|
||||||
|
refreshToken: "",
|
||||||
|
matchedBy: "psid",
|
||||||
|
fbAsid: "",
|
||||||
|
fbPsid: "e2e-facebook-psid",
|
||||||
|
hasCompleteFacebookIdentity: false,
|
||||||
|
isGuest: true,
|
||||||
|
user: e2eUser,
|
||||||
|
userId: e2eUser.id,
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import { mockCoreApis } from "@e2e/fixtures/api-mocks";
|
||||||
|
import { clearBrowserState } from "@e2e/fixtures/test-helpers";
|
||||||
|
|
||||||
|
const psid = "e2e-facebook-psid";
|
||||||
|
|
||||||
|
test.beforeEach(async ({ baseURL, context, page }) => {
|
||||||
|
await clearBrowserState(context, page, baseURL);
|
||||||
|
await mockCoreApis(page, { psidLoginFlow: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("user enters from an external PSID link and becomes a guest", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const psidLoginRequestPromise = page.waitForRequest(
|
||||||
|
"**/api/auth/login/facebook/psid",
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto(`/external-entry?target=chat&psid=${psid}`);
|
||||||
|
|
||||||
|
const psidLoginRequest = await psidLoginRequestPromise;
|
||||||
|
expect(psidLoginRequest.method()).toBe("POST");
|
||||||
|
expect(psidLoginRequest.postDataJSON()).toMatchObject({
|
||||||
|
psid,
|
||||||
|
bindToGuest: true,
|
||||||
|
});
|
||||||
|
expect(psidLoginRequest.postDataJSON().deviceId).toEqual(expect.any(String));
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/chat$/);
|
||||||
|
await expect(page.getByRole("textbox", { name: "Message" })).toBeVisible();
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(() =>
|
||||||
|
page.evaluate(() => ({
|
||||||
|
loginProvider: localStorage.getItem("cozsweet:login_provider"),
|
||||||
|
guestToken: localStorage.getItem("cozsweet:guest_token"),
|
||||||
|
psid: localStorage.getItem("cozsweet:psid"),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.toMatchObject({
|
||||||
|
loginProvider: "guest",
|
||||||
|
guestToken: "e2e-psid-guest-token",
|
||||||
|
psid,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -44,6 +44,7 @@ export default function ExternalEntryPersist({
|
|||||||
const authDispatch = useAuthDispatch();
|
const authDispatch = useAuthDispatch();
|
||||||
const [hasPersisted, setHasPersisted] = useState(false);
|
const [hasPersisted, setHasPersisted] = useState(false);
|
||||||
const hasNavigatedRef = useRef(false);
|
const hasNavigatedRef = useRef(false);
|
||||||
|
const hasReinitializedForPsidRef = useRef(false);
|
||||||
const targetRoute = resolveExternalEntryTarget({ target });
|
const targetRoute = resolveExternalEntryTarget({ target });
|
||||||
const destination = resolveExternalEntryDestination({ target });
|
const destination = resolveExternalEntryDestination({ target });
|
||||||
const resolvedPromotionType = resolveExternalEntryPromotionType({
|
const resolvedPromotionType = resolveExternalEntryPromotionType({
|
||||||
@@ -94,6 +95,16 @@ export default function ExternalEntryPersist({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasNavigatedRef.current || !hasPersisted) return;
|
if (hasNavigatedRef.current || !hasPersisted) return;
|
||||||
|
|
||||||
|
// AuthStatusChecker can initialize before this entry has persisted PSID.
|
||||||
|
// Re-run the check so PSID direct login is not skipped by that race.
|
||||||
|
if (hasValue(psid) && !hasReinitializedForPsidRef.current) {
|
||||||
|
if (!authState.hasInitialized || authState.isLoading) return;
|
||||||
|
hasReinitializedForPsidRef.current = true;
|
||||||
|
authDispatch({ type: "AuthInit" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!authState.hasInitialized || authState.isLoading) return;
|
if (!authState.hasInitialized || authState.isLoading) return;
|
||||||
hasNavigatedRef.current = true;
|
hasNavigatedRef.current = true;
|
||||||
|
|
||||||
@@ -138,3 +149,7 @@ export default function ExternalEntryPersist({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasValue(value: string | null): value is string {
|
||||||
|
return typeof value === "string" && value.trim().length > 0;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user