213 lines
6.1 KiB
TypeScript
213 lines
6.1 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { render, screen, waitFor, within } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { beforeEach, expect, test, vi } from 'vitest';
|
|
|
|
import type { AuthUser } from '../../services/authService';
|
|
import { AuthGate } from './AuthGate';
|
|
import { useAuthUi } from './AuthUiContext';
|
|
|
|
const authMocks = vi.hoisted(() => ({
|
|
getStoredAccessToken: vi.fn(),
|
|
ensureAutoAuthUser: vi.fn(),
|
|
getAuthLoginOptions: vi.fn(),
|
|
loginWithPhoneCode: vi.fn(),
|
|
sendPhoneLoginCode: vi.fn(),
|
|
startWechatLogin: vi.fn(),
|
|
consumeAuthCallbackResult: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../services/apiClient', () => ({
|
|
AUTH_STATE_EVENT: 'genarrative-auth-state-changed',
|
|
getStoredAccessToken: authMocks.getStoredAccessToken,
|
|
}));
|
|
|
|
vi.mock('../../services/authService', () => ({
|
|
bindWechatPhone: vi.fn(),
|
|
changePhoneNumber: vi.fn(),
|
|
consumeAuthCallbackResult: authMocks.consumeAuthCallbackResult,
|
|
ensureAutoAuthUser: authMocks.ensureAutoAuthUser,
|
|
getAuthAuditLogs: vi.fn(),
|
|
getAuthLoginOptions: authMocks.getAuthLoginOptions,
|
|
getAuthRiskBlocks: vi.fn(),
|
|
getAuthSessions: vi.fn(),
|
|
getCaptchaChallengeFromError: vi.fn(() => null),
|
|
getCurrentAuthUser: vi.fn(),
|
|
liftAuthRiskBlock: vi.fn(),
|
|
loginWithPhoneCode: authMocks.loginWithPhoneCode,
|
|
logoutAllAuthSessions: vi.fn(),
|
|
logoutAuthUser: vi.fn(),
|
|
revokeAuthSession: vi.fn(),
|
|
sendPhoneLoginCode: authMocks.sendPhoneLoginCode,
|
|
startWechatLogin: authMocks.startWechatLogin,
|
|
}));
|
|
|
|
vi.mock('../../hooks/useGameSettings', () => ({
|
|
useGameSettings: () => ({
|
|
musicVolume: 0.42,
|
|
setMusicVolume: () => {},
|
|
platformTheme: 'light',
|
|
setPlatformTheme: () => {},
|
|
hasHydratedSettings: true,
|
|
isHydratingSettings: false,
|
|
isPersistingSettings: false,
|
|
settingsError: null,
|
|
}),
|
|
}));
|
|
|
|
vi.mock('./AccountModal', () => ({
|
|
AccountModal: () => null,
|
|
}));
|
|
|
|
vi.mock('./BindPhoneScreen', () => ({
|
|
BindPhoneScreen: () => <div>绑定手机号</div>,
|
|
}));
|
|
|
|
const mockUser: AuthUser = {
|
|
id: 'user-1',
|
|
username: 'tester',
|
|
displayName: '测试玩家',
|
|
phoneNumberMasked: '138****8000',
|
|
loginMethod: 'phone',
|
|
bindingStatus: 'active',
|
|
wechatBound: false,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
authMocks.getStoredAccessToken.mockReturnValue(null);
|
|
authMocks.consumeAuthCallbackResult.mockReturnValue(null);
|
|
authMocks.loginWithPhoneCode.mockResolvedValue(mockUser);
|
|
authMocks.sendPhoneLoginCode.mockResolvedValue({
|
|
cooldownSeconds: 60,
|
|
expiresInSeconds: 300,
|
|
});
|
|
authMocks.startWechatLogin.mockResolvedValue(undefined);
|
|
authMocks.ensureAutoAuthUser.mockResolvedValue({
|
|
user: mockUser,
|
|
credentials: {
|
|
username: 'guest_tester',
|
|
password: 'auto_password',
|
|
},
|
|
});
|
|
});
|
|
|
|
function ProtectedActionButton({ onAuthenticated }: { onAuthenticated: () => void }) {
|
|
const authUi = useAuthUi();
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
authUi?.requireAuth(onAuthenticated);
|
|
}}
|
|
>
|
|
进入作品
|
|
</button>
|
|
);
|
|
}
|
|
|
|
test('auth gate keeps platform content visible when phone login is available', async () => {
|
|
authMocks.getAuthLoginOptions.mockResolvedValue({
|
|
availableLoginMethods: ['phone'],
|
|
});
|
|
|
|
render(
|
|
<AuthGate>
|
|
<div>应用内容</div>
|
|
</AuthGate>,
|
|
);
|
|
|
|
expect(await screen.findByText('应用内容')).toBeTruthy();
|
|
expect(screen.queryByRole('button', { name: '登录' })).toBeNull();
|
|
expect(screen.queryByText('先登录账号,再同步你的冒险进度。')).toBeNull();
|
|
expect(authMocks.ensureAutoAuthUser).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('auth gate does not auto-create a guest account when dev guest switch is not explicitly enabled', async () => {
|
|
authMocks.getAuthLoginOptions.mockResolvedValue({
|
|
availableLoginMethods: [],
|
|
});
|
|
|
|
render(
|
|
<AuthGate>
|
|
<div>应用内容</div>
|
|
</AuthGate>,
|
|
);
|
|
|
|
expect(await screen.findByText('应用内容')).toBeTruthy();
|
|
expect(authMocks.ensureAutoAuthUser).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('auth gate opens a login modal for protected actions and resumes after login', async () => {
|
|
const user = userEvent.setup();
|
|
const onAuthenticated = vi.fn();
|
|
|
|
authMocks.getAuthLoginOptions.mockResolvedValue({
|
|
availableLoginMethods: ['phone'],
|
|
});
|
|
|
|
render(
|
|
<AuthGate>
|
|
<ProtectedActionButton onAuthenticated={onAuthenticated} />
|
|
</AuthGate>,
|
|
);
|
|
|
|
await user.click(await screen.findByRole('button', { name: '进入作品' }));
|
|
|
|
const dialog = screen.getByRole('dialog', { name: '登录账号' });
|
|
expect(dialog).toBeTruthy();
|
|
expect(screen.queryByText('先登录账号,再同步你的冒险进度。')).toBeNull();
|
|
|
|
await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
|
|
await user.type(within(dialog).getByLabelText('验证码'), '123456');
|
|
await user.click(within(dialog).getByRole('button', { name: '登录' }));
|
|
|
|
await waitFor(() => {
|
|
expect(authMocks.loginWithPhoneCode).toHaveBeenCalledWith(
|
|
'13800000000',
|
|
'123456',
|
|
);
|
|
expect(onAuthenticated).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
expect(screen.queryByRole('dialog', { name: '登录账号' })).toBeNull();
|
|
});
|
|
|
|
test('auth gate shows sms send feedback in the login modal', async () => {
|
|
const user = userEvent.setup();
|
|
|
|
authMocks.getAuthLoginOptions.mockResolvedValue({
|
|
availableLoginMethods: ['phone'],
|
|
});
|
|
|
|
render(
|
|
<AuthGate>
|
|
<ProtectedActionButton onAuthenticated={vi.fn()} />
|
|
</AuthGate>,
|
|
);
|
|
|
|
await user.click(await screen.findByRole('button', { name: '进入作品' }));
|
|
|
|
const dialog = screen.getByRole('dialog', { name: '登录账号' });
|
|
await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
|
|
await user.click(within(dialog).getByRole('button', { name: '获取验证码' }));
|
|
|
|
await waitFor(() => {
|
|
expect(authMocks.sendPhoneLoginCode).toHaveBeenCalledWith(
|
|
'13800000000',
|
|
'login',
|
|
{
|
|
challengeId: undefined,
|
|
answer: '',
|
|
},
|
|
);
|
|
});
|
|
|
|
expect(
|
|
within(dialog).getByText('验证码已发送,有效期约 5 分钟。'),
|
|
).toBeTruthy();
|
|
expect(within(dialog).getByRole('button', { name: '60s' })).toBeTruthy();
|
|
});
|