mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 10:55:31 -06:00
fix(zalouser): cancel declined QR login
This commit is contained in:
@@ -11,6 +11,7 @@ import "./zalo-js.test-mocks.js";
|
||||
import { zalouserSetupWizard } from "./setup-surface.js";
|
||||
import { zalouserSetupPlugin } from "./setup-test-helpers.js";
|
||||
import {
|
||||
cancelZaloQrLoginMock,
|
||||
checkZaloAuthenticatedMock,
|
||||
logoutZaloProfileMock,
|
||||
resolveZaloAllowFromEntriesMock,
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
} from "./zalo-js.test-mocks.js";
|
||||
|
||||
const zalouserConfigure = createPluginSetupWizardConfigure(zalouserSetupPlugin);
|
||||
const PNG_1X1 =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=";
|
||||
|
||||
async function runSetup(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
@@ -239,6 +242,30 @@ describe("zalouser setup wizard", () => {
|
||||
expect(waitForZaloQrLoginMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cancels the profile-owned QR login when confirmation is declined", async () => {
|
||||
checkZaloAuthenticatedMock.mockResolvedValueOnce(false);
|
||||
startZaloQrLoginMock.mockResolvedValueOnce({
|
||||
message: "Scan this QR with the Zalo app.",
|
||||
qrDataUrl: `data:image/png;base64,${PNG_1X1}`,
|
||||
});
|
||||
cancelZaloQrLoginMock.mockClear();
|
||||
waitForZaloQrLoginMock.mockClear();
|
||||
const prompter = createTestWizardPrompter({
|
||||
confirm: vi.fn(async ({ message }: { message: string }) => {
|
||||
if (message === "Login via QR code now?") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
});
|
||||
|
||||
await runSetup({ prompter });
|
||||
|
||||
expect(cancelZaloQrLoginMock).toHaveBeenCalledOnce();
|
||||
expect(cancelZaloQrLoginMock).toHaveBeenCalledWith("default");
|
||||
expect(waitForZaloQrLoginMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "first login", authenticated: false, expectedLogouts: 0 },
|
||||
{ name: "forced re-login", authenticated: true, expectedLogouts: 1 },
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "./accounts.js";
|
||||
import { writeQrDataUrlToTempFile } from "./qr-temp-file.js";
|
||||
import {
|
||||
cancelZaloQrLogin,
|
||||
logoutZaloProfile,
|
||||
resolveZaloAllowFromEntries,
|
||||
resolveZaloGroupsByEntries,
|
||||
@@ -315,6 +316,7 @@ async function runZalouserQrLogin(params: {
|
||||
initialValue: true,
|
||||
});
|
||||
if (!scanned) {
|
||||
cancelZaloQrLogin(params.profile);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
type StoredZaloCredentials,
|
||||
} from "./session-state.js";
|
||||
import {
|
||||
cancelZaloQrLogin,
|
||||
checkZaloAuthenticated,
|
||||
listZaloFriends,
|
||||
sendZaloLink,
|
||||
@@ -191,6 +192,66 @@ describe("zalouser credential persistence", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not persist credentials after the profile-owned QR login is cancelled", async () => {
|
||||
const stateDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-zalouser-credentials-"));
|
||||
const profile = "qr-cancelled";
|
||||
const abort = vi.fn();
|
||||
let resolveLogin!: (api: API) => void;
|
||||
const login = new Promise<API>((resolve) => {
|
||||
resolveLogin = resolve;
|
||||
});
|
||||
let callback: ((event: LoginQRCallbackEvent) => unknown) | undefined;
|
||||
const api = createMockApi({
|
||||
imei: "cancelled-imei",
|
||||
userAgent: "cancelled-agent",
|
||||
cookies: [{ key: "zpsid", value: "cancelled", domain: "chat.zalo.me" }],
|
||||
});
|
||||
createZaloMock.mockResolvedValueOnce({
|
||||
loginQR: async (
|
||||
_options: unknown,
|
||||
eventCallback?: (event: LoginQRCallbackEvent) => unknown,
|
||||
) => {
|
||||
callback = eventCallback;
|
||||
callback?.({
|
||||
type: LoginQRCallbackEventType.QRCodeGenerated,
|
||||
data: { code: "qr-code", image: `data:image/png;base64,${PNG_1X1}` },
|
||||
actions: {
|
||||
saveToFile: vi.fn(async () => undefined),
|
||||
retry: vi.fn(),
|
||||
abort,
|
||||
},
|
||||
});
|
||||
return await login;
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await withEnvAsync({ OPENCLAW_STATE_DIR: stateDir }, async () => {
|
||||
const started = await startZaloQrLogin({ profile, timeoutMs: 1000 });
|
||||
expect(started.qrDataUrl).toBe(`data:image/png;base64,${PNG_1X1}`);
|
||||
|
||||
cancelZaloQrLogin(profile);
|
||||
callback?.({
|
||||
type: LoginQRCallbackEventType.GotLoginInfo,
|
||||
data: {
|
||||
cookie: [{ key: "zpsid", value: "late", domain: "chat.zalo.me" }],
|
||||
imei: "late-imei",
|
||||
userAgent: "late-agent",
|
||||
},
|
||||
actions: null,
|
||||
});
|
||||
resolveLogin(api);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(abort).toHaveBeenCalledOnce();
|
||||
expect(loadStoredZaloCredentials(profile)).toBeNull();
|
||||
});
|
||||
} finally {
|
||||
await rm(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a non-PNG QR image and allows an immediate valid retry", async () => {
|
||||
const profile = "qr-invalid-image-retry";
|
||||
const firstAbort = vi.fn();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { vi, type Mock } from "vitest";
|
||||
|
||||
type ZaloJsModule = typeof import("./zalo-js.js");
|
||||
type ZaloJsMocks = {
|
||||
cancelZaloQrLoginMock: Mock<ZaloJsModule["cancelZaloQrLogin"]>;
|
||||
checkZaloAuthenticatedMock: Mock<ZaloJsModule["checkZaloAuthenticated"]>;
|
||||
getZaloUserInfoMock: Mock<ZaloJsModule["getZaloUserInfo"]>;
|
||||
listZaloFriendsMock: Mock<ZaloJsModule["listZaloFriends"]>;
|
||||
@@ -23,6 +24,7 @@ type ZaloJsMocks = {
|
||||
|
||||
const zaloJsMocks = vi.hoisted(
|
||||
(): ZaloJsMocks => ({
|
||||
cancelZaloQrLoginMock: vi.fn(),
|
||||
checkZaloAuthenticatedMock: vi.fn(async () => false),
|
||||
getZaloUserInfoMock: vi.fn(async () => null),
|
||||
listZaloFriendsMock: vi.fn(async () => []),
|
||||
@@ -66,6 +68,7 @@ const zaloJsMocks = vi.hoisted(
|
||||
);
|
||||
|
||||
export const listZaloFriendsMock = zaloJsMocks.listZaloFriendsMock;
|
||||
export const cancelZaloQrLoginMock = zaloJsMocks.cancelZaloQrLoginMock;
|
||||
export const listZaloFriendsMatchingMock = zaloJsMocks.listZaloFriendsMatchingMock;
|
||||
export const listZaloGroupMembersMock = zaloJsMocks.listZaloGroupMembersMock;
|
||||
export const listZaloGroupsMock = zaloJsMocks.listZaloGroupsMock;
|
||||
@@ -79,6 +82,7 @@ export const startZaloQrLoginMock = zaloJsMocks.startZaloQrLoginMock;
|
||||
export const waitForZaloQrLoginMock = zaloJsMocks.waitForZaloQrLoginMock;
|
||||
|
||||
vi.mock("./zalo-js.js", () => ({
|
||||
cancelZaloQrLogin: cancelZaloQrLoginMock,
|
||||
checkZaloAuthenticated: zaloJsMocks.checkZaloAuthenticatedMock,
|
||||
getZaloUserInfo: zaloJsMocks.getZaloUserInfoMock,
|
||||
listZaloFriends: listZaloFriendsMock,
|
||||
|
||||
@@ -757,6 +757,11 @@ function resetQrLogin(profileInput?: string | null): void {
|
||||
activeQrLogins.delete(profile);
|
||||
}
|
||||
|
||||
/** Stop one profile-owned QR attempt without clearing an existing authenticated session. */
|
||||
export function cancelZaloQrLogin(profileInput?: string | null): void {
|
||||
resetQrLogin(profileInput);
|
||||
}
|
||||
|
||||
async function fetchGroupsByIds(api: API, ids: string[]): Promise<Map<string, GroupInfo>> {
|
||||
const result = new Map<string, GroupInfo>();
|
||||
for (let index = 0; index < ids.length; index += GROUP_INFO_CHUNK_SIZE) {
|
||||
|
||||
Reference in New Issue
Block a user