feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { setUserInfoContext } from '@coze-arch/logger';
|
||||
|
||||
import { refreshUserInfoBase, logoutBase, checkLoginBase } from '../factory';
|
||||
import { type UserInfo } from '../../types';
|
||||
import { useUserStore } from '../../store/user';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@coze-arch/logger', () => ({
|
||||
setUserInfoContext: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../store/user', () => ({
|
||||
useUserStore: {
|
||||
getState: vi.fn(),
|
||||
setState: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('factory.ts utility functions', () => {
|
||||
let mockGetState: Mock;
|
||||
let mockSetState: Mock;
|
||||
let mockSetUserInfo: Mock;
|
||||
let mockReset: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSetUserInfo = vi.fn();
|
||||
mockReset = vi.fn();
|
||||
mockGetState = useUserStore.getState as Mock;
|
||||
mockSetState = useUserStore.setState as Mock;
|
||||
mockGetState.mockReturnValue({
|
||||
setUserInfo: mockSetUserInfo,
|
||||
reset: mockReset,
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshUserInfoBase', () => {
|
||||
it('should correctly refresh user information', async () => {
|
||||
const mockUserInfo = {
|
||||
user_id_str: '123',
|
||||
name: 'Test User',
|
||||
} as UserInfo;
|
||||
const mockCheckLogin = vi.fn().mockResolvedValue(mockUserInfo);
|
||||
|
||||
await refreshUserInfoBase(mockCheckLogin);
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledWith({ hasError: false });
|
||||
expect(mockCheckLogin).toHaveBeenCalled();
|
||||
expect(mockSetUserInfo).toHaveBeenCalledWith(mockUserInfo);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logoutBase', () => {
|
||||
it('should correctly execute logout operation', async () => {
|
||||
const mockLogout = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
await logoutBase(mockLogout);
|
||||
|
||||
expect(mockLogout).toHaveBeenCalled();
|
||||
expect(mockReset).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkLoginBase', () => {
|
||||
const mockSetUserInfoContext = setUserInfoContext as Mock;
|
||||
|
||||
it('should correctly handle successful login state', async () => {
|
||||
const mockUserInfo = {
|
||||
user_id_str: '123',
|
||||
name: 'Test User',
|
||||
} as UserInfo;
|
||||
const mockCheckLoginImpl = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ userInfo: mockUserInfo });
|
||||
|
||||
await checkLoginBase(mockCheckLoginImpl);
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledWith({ hasError: false });
|
||||
expect(mockSetUserInfoContext).toHaveBeenCalledWith(mockUserInfo);
|
||||
expect(mockSetState).toHaveBeenCalledWith({
|
||||
userInfo: mockUserInfo,
|
||||
isSettled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly handle login error state', async () => {
|
||||
const mockCheckLoginImpl = vi.fn().mockResolvedValue({ hasError: true });
|
||||
|
||||
await checkLoginBase(mockCheckLoginImpl);
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledWith({ hasError: false });
|
||||
expect(mockSetState).toHaveBeenCalledWith({ hasError: true });
|
||||
expect(mockSetUserInfoContext).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should correctly handle not logged in state', async () => {
|
||||
const mockCheckLoginImpl = vi.fn().mockResolvedValue({ userInfo: null });
|
||||
|
||||
await checkLoginBase(mockCheckLoginImpl);
|
||||
|
||||
expect(mockSetState).toHaveBeenCalledWith({ hasError: false });
|
||||
expect(mockSetState).toHaveBeenCalledWith({
|
||||
userInfo: null,
|
||||
isSettled: true,
|
||||
});
|
||||
expect(mockSetUserInfoContext).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import {
|
||||
getUserInfo,
|
||||
getLoginStatus,
|
||||
resetUserStore,
|
||||
setUserInfo,
|
||||
getUserLabel,
|
||||
getUserAuthInfos,
|
||||
subscribeUserAuthInfos,
|
||||
usernameRegExpValidate,
|
||||
} from '../index';
|
||||
import { useUserStore } from '../../store/user';
|
||||
|
||||
// Mock useUserStore
|
||||
vi.mock('../../store/user', () => ({
|
||||
useUserStore: {
|
||||
getState: vi.fn(),
|
||||
setState: vi.fn(), // Though not directly used by all utils, good to have for setUserInfo
|
||||
subscribe: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock I18n
|
||||
vi.mock('@coze-arch/i18n', () => ({
|
||||
I18n: {
|
||||
t: vi.fn(key => key), // Simple mock that returns the key
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Utility functions from utils/index.ts', () => {
|
||||
let mockGetState: Mock;
|
||||
let mockSubscribe: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockGetState = useUserStore.getState as Mock;
|
||||
mockSubscribe = useUserStore.subscribe as Mock;
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
it('should return userInfo from userStore.getState()', () => {
|
||||
const mockUser = { user_id_str: 'testUser', name: 'Test User' };
|
||||
mockGetState.mockReturnValue({ userInfo: mockUser });
|
||||
expect(getUserInfo()).toEqual(mockUser);
|
||||
expect(mockGetState).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLoginStatus', () => {
|
||||
it('should return "settling" if store is not settled', () => {
|
||||
mockGetState.mockReturnValue({ isSettled: false, userInfo: null });
|
||||
expect(getLoginStatus()).toBe('settling');
|
||||
});
|
||||
|
||||
it('should return "not_login" if store is settled and no userInfo', () => {
|
||||
mockGetState.mockReturnValue({ isSettled: true, userInfo: null });
|
||||
expect(getLoginStatus()).toBe('not_login');
|
||||
});
|
||||
|
||||
it('should return "not_login" if store is settled and userInfo has no user_id_str', () => {
|
||||
mockGetState.mockReturnValue({
|
||||
isSettled: true,
|
||||
userInfo: { name: 'Test' },
|
||||
});
|
||||
expect(getLoginStatus()).toBe('not_login');
|
||||
});
|
||||
|
||||
it('should return "logined" if store is settled and userInfo has user_id_str', () => {
|
||||
mockGetState.mockReturnValue({
|
||||
isSettled: true,
|
||||
userInfo: { user_id_str: '123' },
|
||||
});
|
||||
expect(getLoginStatus()).toBe('logined');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetUserStore', () => {
|
||||
it('should call reset on userStore.getState()', () => {
|
||||
const mockReset = vi.fn();
|
||||
mockGetState.mockReturnValue({ reset: mockReset });
|
||||
resetUserStore();
|
||||
expect(mockReset).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUserInfo', () => {
|
||||
it('should call setUserInfo on userStore.getState() with the provided user info', () => {
|
||||
const mockSetUserInfo = vi.fn();
|
||||
mockGetState.mockReturnValue({ setUserInfo: mockSetUserInfo });
|
||||
const newUser = { user_id_str: 'newUser', name: 'New User' };
|
||||
setUserInfo(newUser);
|
||||
expect(mockSetUserInfo).toHaveBeenCalledWith(newUser);
|
||||
});
|
||||
|
||||
it('should call setUserInfo on userStore.getState() with null', () => {
|
||||
const mockSetUserInfo = vi.fn();
|
||||
mockGetState.mockReturnValue({ setUserInfo: mockSetUserInfo });
|
||||
setUserInfo(null);
|
||||
expect(mockSetUserInfo).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserLabel', () => {
|
||||
it('should return userLabel from userStore.getState()', () => {
|
||||
const mockLabel = { label_type: 1, text: 'VIP' };
|
||||
mockGetState.mockReturnValue({ userLabel: mockLabel });
|
||||
expect(getUserLabel()).toEqual(mockLabel);
|
||||
expect(mockGetState).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserAuthInfos', () => {
|
||||
it('should call getUserAuthInfos on userStore.getState()', async () => {
|
||||
const mockGetUserAuthInfos = vi.fn().mockResolvedValue([]);
|
||||
mockGetState.mockReturnValue({ getUserAuthInfos: mockGetUserAuthInfos });
|
||||
await getUserAuthInfos();
|
||||
expect(mockGetUserAuthInfos).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribeUserAuthInfos', () => {
|
||||
it('should call userStore.subscribe with a selector for userAuthInfos and the callback', () => {
|
||||
const callback = vi.fn();
|
||||
const mockUserAuthInfos = [
|
||||
{ auth_type: 'email', auth_key: 'test@example.com' },
|
||||
];
|
||||
// Mock the subscribe implementation to immediately call the listener with selected state
|
||||
mockSubscribe.mockImplementation(
|
||||
(selector, cb) => vi.fn(), // Return unsubscribe function
|
||||
);
|
||||
|
||||
subscribeUserAuthInfos(callback);
|
||||
|
||||
expect(mockSubscribe).toHaveBeenCalledTimes(1);
|
||||
// Check that the selector passed to subscribe correctly extracts userAuthInfos
|
||||
const selectorArg = mockSubscribe.mock.calls[0][0];
|
||||
expect(selectorArg({ userAuthInfos: mockUserAuthInfos })).toEqual(
|
||||
mockUserAuthInfos,
|
||||
);
|
||||
expect(mockSubscribe.mock.calls[0][1]).toBe(callback);
|
||||
});
|
||||
});
|
||||
|
||||
describe('usernameRegExpValidate', () => {
|
||||
it('should return null for valid usernames', () => {
|
||||
expect(usernameRegExpValidate('validUser123')).toBeNull();
|
||||
expect(usernameRegExpValidate('another_valid_user')).toBeNull();
|
||||
expect(usernameRegExpValidate('USER')).toBeNull();
|
||||
expect(usernameRegExpValidate('1234')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return "username_invalid_letter" for usernames with invalid characters', () => {
|
||||
(I18n.t as Mock).mockReturnValueOnce('username_invalid_letter');
|
||||
expect(usernameRegExpValidate('invalid-char')).toBe(
|
||||
'username_invalid_letter',
|
||||
);
|
||||
(I18n.t as Mock).mockReturnValueOnce('username_invalid_letter');
|
||||
expect(usernameRegExpValidate('invalid char')).toBe(
|
||||
'username_invalid_letter',
|
||||
);
|
||||
(I18n.t as Mock).mockReturnValueOnce('username_invalid_letter');
|
||||
expect(usernameRegExpValidate('!@#$%^')).toBe('username_invalid_letter');
|
||||
expect(I18n.t).toHaveBeenCalledWith('username_invalid_letter');
|
||||
});
|
||||
|
||||
it('should return "username_too_short" for usernames shorter than minLength (4)', () => {
|
||||
(I18n.t as Mock).mockReturnValueOnce('username_too_short');
|
||||
expect(usernameRegExpValidate('abc')).toBe('username_too_short');
|
||||
(I18n.t as Mock).mockReturnValueOnce('username_too_short');
|
||||
expect(usernameRegExpValidate('us')).toBe('username_too_short');
|
||||
expect(I18n.t).toHaveBeenCalledWith('username_too_short');
|
||||
});
|
||||
|
||||
it('should return "username_invalid_letter" if invalid char before checking length', () => {
|
||||
// This case tests if the invalid character check takes precedence
|
||||
(I18n.t as Mock).mockReturnValueOnce('username_invalid_letter');
|
||||
expect(usernameRegExpValidate('a-b')).toBe('username_invalid_letter'); // Length 3, but invalid char
|
||||
expect(I18n.t).toHaveBeenCalledWith('username_invalid_letter');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { setUserInfoContext } from '@coze-arch/logger';
|
||||
|
||||
import { type UserInfo } from '../types';
|
||||
import { useUserStore } from '../store/user';
|
||||
|
||||
/**
|
||||
* 主动触发刷新用户信息
|
||||
* @param checkLogin 登录检查函数
|
||||
*/
|
||||
export const refreshUserInfoBase = async (
|
||||
checkLogin: () => Promise<UserInfo>,
|
||||
) => {
|
||||
useUserStore.setState({
|
||||
hasError: false,
|
||||
});
|
||||
const userInfo = await checkLogin();
|
||||
useUserStore.getState().setUserInfo(userInfo);
|
||||
};
|
||||
|
||||
export const logoutBase = async (logout: () => Promise<void>) => {
|
||||
await logout();
|
||||
useUserStore.getState().reset();
|
||||
};
|
||||
|
||||
export const checkLoginBase = async (
|
||||
checkLoginImpl: () => Promise<{
|
||||
userInfo?: UserInfo;
|
||||
hasError?: boolean;
|
||||
}>,
|
||||
) => {
|
||||
useUserStore.setState({
|
||||
hasError: false,
|
||||
});
|
||||
const { userInfo, hasError } = await checkLoginImpl();
|
||||
if (hasError) {
|
||||
useUserStore.setState({
|
||||
hasError: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (userInfo) {
|
||||
setUserInfoContext(userInfo);
|
||||
}
|
||||
useUserStore.setState({
|
||||
userInfo,
|
||||
isSettled: true,
|
||||
});
|
||||
};
|
||||
66
frontend/packages/foundation/account-base/src/utils/index.ts
Normal file
66
frontend/packages/foundation/account-base/src/utils/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type UserAuthInfo } from '@coze-arch/idl/developer_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { type UserInfo, type LoginStatus } from '../types';
|
||||
import { useUserStore } from '../store/user';
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @returns UserInfo 用户信息
|
||||
*/
|
||||
export const getUserInfo = () => useUserStore.getState().userInfo;
|
||||
|
||||
/**
|
||||
* 获取登录状态
|
||||
* @returns LoginStatus 登录状态
|
||||
*/
|
||||
export const getLoginStatus = (): LoginStatus => {
|
||||
const state = useUserStore.getState();
|
||||
if (state.isSettled) {
|
||||
return state.userInfo?.user_id_str ? 'logined' : 'not_login';
|
||||
}
|
||||
return 'settling';
|
||||
};
|
||||
|
||||
export const resetUserStore = () => useUserStore.getState().reset();
|
||||
|
||||
export const setUserInfo = (userInfo: UserInfo | null) =>
|
||||
useUserStore.getState().setUserInfo(userInfo);
|
||||
|
||||
export const getUserLabel = () => useUserStore.getState().userLabel;
|
||||
|
||||
export const getUserAuthInfos = () =>
|
||||
useUserStore.getState().getUserAuthInfos();
|
||||
|
||||
export const subscribeUserAuthInfos = (
|
||||
callback: (state: UserAuthInfo[], prev: UserAuthInfo[]) => void,
|
||||
) => useUserStore.subscribe(state => state.userAuthInfos, callback);
|
||||
|
||||
const usernameRegExp = /^[0-9A-Za-z_]+$/;
|
||||
const minLength = 4;
|
||||
export const usernameRegExpValidate = (value: string) => {
|
||||
if (!usernameRegExp.exec(value)) {
|
||||
return I18n.t('username_invalid_letter');
|
||||
}
|
||||
if (value.length < minLength) {
|
||||
return I18n.t('username_too_short');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
Reference in New Issue
Block a user