forked from datawhale/whale-town-end
feat: 添加JWT认证系统和Zulip用户管理服务
- 新增JWT认证守卫(JwtAuthGuard)和当前用户装饰器(CurrentUser) - 添加JWT使用示例和完整的认证流程文档 - 实现Zulip用户管理服务,支持用户查询、验证和管理 - 实现Zulip用户注册服务,支持新用户创建和注册流程 - 添加完整的单元测试覆盖 - 新增真实环境测试脚本,验证Zulip API集成 - 更新.gitignore,排除.kiro目录 主要功能: - JWT令牌验证和用户信息提取 - 用户存在性检查和信息获取 - Zulip API集成和错误处理 - 完整的测试覆盖和文档
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -44,3 +44,5 @@ coverage/
|
|||||||
|
|
||||||
# Redis数据文件(本地开发用)
|
# Redis数据文件(本地开发用)
|
||||||
redis-data/
|
redis-data/
|
||||||
|
|
||||||
|
.kiro/
|
||||||
39
src/business/auth/decorators/current-user.decorator.ts
Normal file
39
src/business/auth/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 当前用户装饰器
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 从请求上下文中提取当前认证用户信息
|
||||||
|
* - 简化控制器中获取用户信息的操作
|
||||||
|
*
|
||||||
|
* 使用示例:
|
||||||
|
* ```typescript
|
||||||
|
* @Get('profile')
|
||||||
|
* @UseGuards(JwtAuthGuard)
|
||||||
|
* getProfile(@CurrentUser() user: JwtPayload) {
|
||||||
|
* return { user };
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @author kiro-ai
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-01-05
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { AuthenticatedRequest, JwtPayload } from '../guards/jwt-auth.guard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前用户装饰器
|
||||||
|
*
|
||||||
|
* @param data 可选的属性名,用于获取用户对象的特定属性
|
||||||
|
* @param ctx 执行上下文
|
||||||
|
* @returns 用户信息或用户的特定属性
|
||||||
|
*/
|
||||||
|
export const CurrentUser = createParamDecorator(
|
||||||
|
(data: keyof JwtPayload | undefined, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest<AuthenticatedRequest>();
|
||||||
|
const user = request.user;
|
||||||
|
|
||||||
|
return data ? user?.[data] : user;
|
||||||
|
},
|
||||||
|
);
|
||||||
128
src/business/auth/examples/jwt-usage-example.ts
Normal file
128
src/business/auth/examples/jwt-usage-example.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* JWT 使用示例
|
||||||
|
*
|
||||||
|
* 展示如何在控制器中使用 JWT 认证守卫和当前用户装饰器
|
||||||
|
*
|
||||||
|
* @author kiro-ai
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-01-05
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Controller, Get, UseGuards, Post, Body } from '@nestjs/common';
|
||||||
|
import { JwtAuthGuard, JwtPayload } from '../guards/jwt-auth.guard';
|
||||||
|
import { CurrentUser } from '../decorators/current-user.decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例控制器 - 展示 JWT 认证的使用方法
|
||||||
|
*/
|
||||||
|
@Controller('example')
|
||||||
|
export class ExampleController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公开接口 - 无需认证
|
||||||
|
*/
|
||||||
|
@Get('public')
|
||||||
|
getPublicData() {
|
||||||
|
return {
|
||||||
|
message: '这是一个公开接口,无需认证',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 受保护的接口 - 需要 JWT 认证
|
||||||
|
*
|
||||||
|
* 请求头示例:
|
||||||
|
* Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
*/
|
||||||
|
@Get('protected')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
getProtectedData(@CurrentUser() user: JwtPayload) {
|
||||||
|
return {
|
||||||
|
message: '这是一个受保护的接口,需要有效的 JWT 令牌',
|
||||||
|
user: {
|
||||||
|
id: user.sub,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role,
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户信息
|
||||||
|
*/
|
||||||
|
@Get('profile')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
getUserProfile(@CurrentUser() user: JwtPayload) {
|
||||||
|
return {
|
||||||
|
profile: {
|
||||||
|
userId: user.sub,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role,
|
||||||
|
tokenIssuedAt: new Date(user.iat * 1000).toISOString(),
|
||||||
|
tokenExpiresAt: new Date(user.exp * 1000).toISOString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的特定属性
|
||||||
|
*/
|
||||||
|
@Get('username')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
getUsername(@CurrentUser('username') username: string) {
|
||||||
|
return {
|
||||||
|
username,
|
||||||
|
message: `你好,${username}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要特定角色的接口
|
||||||
|
*/
|
||||||
|
@Post('admin-only')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
adminOnlyAction(@CurrentUser() user: JwtPayload, @Body() data: any) {
|
||||||
|
// 检查用户角色
|
||||||
|
if (user.role !== 1) { // 假设 1 是管理员角色
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '权限不足,仅管理员可访问',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: '管理员操作执行成功',
|
||||||
|
data,
|
||||||
|
operator: user.username,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用说明:
|
||||||
|
*
|
||||||
|
* 1. 首先调用登录接口获取 JWT 令牌:
|
||||||
|
* POST /auth/login
|
||||||
|
* {
|
||||||
|
* "identifier": "username",
|
||||||
|
* "password": "password"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* 2. 从响应中获取 access_token
|
||||||
|
*
|
||||||
|
* 3. 在后续请求中添加 Authorization 头:
|
||||||
|
* Authorization: Bearer <access_token>
|
||||||
|
*
|
||||||
|
* 4. 访问受保护的接口:
|
||||||
|
* GET /example/protected
|
||||||
|
* GET /example/profile
|
||||||
|
* GET /example/username
|
||||||
|
* POST /example/admin-only
|
||||||
|
*
|
||||||
|
* 错误处理:
|
||||||
|
* - 401 Unauthorized: 令牌缺失或无效
|
||||||
|
* - 403 Forbidden: 令牌有效但权限不足
|
||||||
|
*/
|
||||||
83
src/business/auth/guards/jwt-auth.guard.ts
Normal file
83
src/business/auth/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* JWT 认证守卫
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 验证请求中的 JWT 令牌
|
||||||
|
* - 提取用户信息并添加到请求上下文
|
||||||
|
* - 保护需要认证的路由
|
||||||
|
*
|
||||||
|
* @author kiro-ai
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-01-05
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
UnauthorizedException,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 载荷接口
|
||||||
|
*/
|
||||||
|
export interface JwtPayload {
|
||||||
|
sub: string; // 用户ID
|
||||||
|
username: string;
|
||||||
|
role: number;
|
||||||
|
iat: number; // 签发时间
|
||||||
|
exp: number; // 过期时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展的请求接口,包含用户信息
|
||||||
|
*/
|
||||||
|
export interface AuthenticatedRequest extends Request {
|
||||||
|
user: JwtPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard implements CanActivate {
|
||||||
|
private readonly logger = new Logger(JwtAuthGuard.name);
|
||||||
|
|
||||||
|
constructor(private readonly jwtService: JwtService) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest<Request>();
|
||||||
|
const token = this.extractTokenFromHeader(request);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
this.logger.warn('访问被拒绝:缺少认证令牌');
|
||||||
|
throw new UnauthorizedException('缺少认证令牌');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证并解码 JWT 令牌
|
||||||
|
const payload = await this.jwtService.verifyAsync<JwtPayload>(token);
|
||||||
|
|
||||||
|
// 将用户信息添加到请求对象
|
||||||
|
(request as AuthenticatedRequest).user = payload;
|
||||||
|
|
||||||
|
this.logger.log(`用户认证成功: ${payload.username} (ID: ${payload.sub})`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||||
|
this.logger.warn(`JWT 令牌验证失败: ${errorMessage}`);
|
||||||
|
throw new UnauthorizedException('无效的认证令牌');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从请求头中提取 JWT 令牌
|
||||||
|
*
|
||||||
|
* @param request 请求对象
|
||||||
|
* @returns JWT 令牌或 undefined
|
||||||
|
*/
|
||||||
|
private extractTokenFromHeader(request: Request): string | undefined {
|
||||||
|
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||||
|
return type === 'Bearer' ? token : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
388
src/core/zulip/services/user_management.service.spec.ts
Normal file
388
src/core/zulip/services/user_management.service.spec.ts
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
/**
|
||||||
|
* Zulip用户管理服务测试
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 测试UserManagementService的核心功能
|
||||||
|
* - 测试用户查询和验证逻辑
|
||||||
|
* - 测试错误处理和边界情况
|
||||||
|
*
|
||||||
|
* @author angjustinl
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-01-06
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { UserManagementService, UserQueryRequest, UserValidationRequest } from './user_management.service';
|
||||||
|
import { IZulipConfigService } from '../interfaces/zulip-core.interfaces';
|
||||||
|
|
||||||
|
// 模拟fetch
|
||||||
|
global.fetch = jest.fn();
|
||||||
|
|
||||||
|
describe('UserManagementService', () => {
|
||||||
|
let service: UserManagementService;
|
||||||
|
let mockConfigService: jest.Mocked<IZulipConfigService>;
|
||||||
|
let mockFetch: jest.MockedFunction<typeof fetch>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// 重置fetch模拟
|
||||||
|
mockFetch = fetch as jest.MockedFunction<typeof fetch>;
|
||||||
|
mockFetch.mockClear();
|
||||||
|
|
||||||
|
// 创建模拟的配置服务
|
||||||
|
mockConfigService = {
|
||||||
|
getZulipConfig: jest.fn().mockReturnValue({
|
||||||
|
zulipServerUrl: 'https://test.zulip.com',
|
||||||
|
zulipBotEmail: 'bot@test.com',
|
||||||
|
zulipBotApiKey: 'test-api-key',
|
||||||
|
}),
|
||||||
|
getMapIdByStream: jest.fn(),
|
||||||
|
getStreamByMap: jest.fn(),
|
||||||
|
getMapConfig: jest.fn(),
|
||||||
|
hasMap: jest.fn(),
|
||||||
|
getAllMapIds: jest.fn(),
|
||||||
|
getMapConfigByStream: jest.fn(),
|
||||||
|
getAllStreams: jest.fn(),
|
||||||
|
hasStream: jest.fn(),
|
||||||
|
findObjectByTopic: jest.fn(),
|
||||||
|
getObjectsInMap: jest.fn(),
|
||||||
|
getTopicByObject: jest.fn(),
|
||||||
|
findNearbyObject: jest.fn(),
|
||||||
|
reloadConfig: jest.fn(),
|
||||||
|
validateConfig: jest.fn(),
|
||||||
|
getAllMapConfigs: jest.fn(),
|
||||||
|
getConfigStats: jest.fn(),
|
||||||
|
getConfigFilePath: jest.fn(),
|
||||||
|
configFileExists: jest.fn(),
|
||||||
|
enableConfigWatcher: jest.fn(),
|
||||||
|
disableConfigWatcher: jest.fn(),
|
||||||
|
isConfigWatcherEnabled: jest.fn(),
|
||||||
|
getFullConfiguration: jest.fn(),
|
||||||
|
updateConfigValue: jest.fn(),
|
||||||
|
exportMapConfig: jest.fn(),
|
||||||
|
} as jest.Mocked<IZulipConfigService>;
|
||||||
|
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
UserManagementService,
|
||||||
|
{
|
||||||
|
provide: 'ZULIP_CONFIG_SERVICE',
|
||||||
|
useValue: mockConfigService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<UserManagementService>(UserManagementService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确初始化服务', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkUserExists - 检查用户是否存在', () => {
|
||||||
|
it('应该正确检查存在的用户', async () => {
|
||||||
|
// 模拟API响应
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
user_id: 1,
|
||||||
|
email: 'test@example.com',
|
||||||
|
full_name: 'Test User',
|
||||||
|
is_active: true,
|
||||||
|
is_admin: false,
|
||||||
|
is_bot: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const result = await service.checkUserExists('test@example.com');
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
'https://test.zulip.com/api/v1/users',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'GET',
|
||||||
|
headers: expect.objectContaining({
|
||||||
|
'Authorization': expect.stringContaining('Basic'),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确检查不存在的用户', async () => {
|
||||||
|
// 模拟API响应
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
user_id: 1,
|
||||||
|
email: 'other@example.com',
|
||||||
|
full_name: 'Other User',
|
||||||
|
is_active: true,
|
||||||
|
is_admin: false,
|
||||||
|
is_bot: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const result = await service.checkUserExists('test@example.com');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理无效邮箱', async () => {
|
||||||
|
const result = await service.checkUserExists('invalid-email');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(mockFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理API调用失败', async () => {
|
||||||
|
// 模拟API失败
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
statusText: 'Internal Server Error',
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const result = await service.checkUserExists('test@example.com');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理网络异常', async () => {
|
||||||
|
// 模拟网络异常
|
||||||
|
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
||||||
|
|
||||||
|
const result = await service.checkUserExists('test@example.com');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUserInfo - 获取用户信息', () => {
|
||||||
|
it('应该成功获取用户信息', async () => {
|
||||||
|
const request: UserQueryRequest = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟API响应
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
user_id: 1,
|
||||||
|
email: 'test@example.com',
|
||||||
|
full_name: 'Test User',
|
||||||
|
is_active: true,
|
||||||
|
is_admin: false,
|
||||||
|
is_bot: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const result = await service.getUserInfo(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.userId).toBe(1);
|
||||||
|
expect(result.email).toBe('test@example.com');
|
||||||
|
expect(result.fullName).toBe('Test User');
|
||||||
|
expect(result.isActive).toBe(true);
|
||||||
|
expect(result.isAdmin).toBe(false);
|
||||||
|
expect(result.isBot).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理用户不存在的情况', async () => {
|
||||||
|
const request: UserQueryRequest = {
|
||||||
|
email: 'nonexistent@example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟API响应
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
members: [],
|
||||||
|
}),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const result = await service.getUserInfo(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('用户不存在');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝无效邮箱', async () => {
|
||||||
|
const request: UserQueryRequest = {
|
||||||
|
email: 'invalid-email',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.getUserInfo(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('邮箱格式无效');
|
||||||
|
expect(mockFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateUserCredentials - 验证用户凭据', () => {
|
||||||
|
it('应该成功验证有效的API Key', async () => {
|
||||||
|
const request: UserValidationRequest = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
apiKey: 'valid-api-key',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟API Key验证响应(第一个调用)
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({}),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
// 模拟用户列表API响应(第二个调用)
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
user_id: 1,
|
||||||
|
email: 'test@example.com',
|
||||||
|
full_name: 'Test User',
|
||||||
|
is_active: true,
|
||||||
|
is_admin: false,
|
||||||
|
is_bot: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const result = await service.validateUserCredentials(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.isValid).toBe(true);
|
||||||
|
expect(result.userId).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝无效的API Key', async () => {
|
||||||
|
const request: UserValidationRequest = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
apiKey: 'invalid-api-key',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟API Key验证失败(第一个调用)
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
status: 401,
|
||||||
|
statusText: 'Unauthorized',
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const result = await service.validateUserCredentials(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.isValid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝空的API Key', async () => {
|
||||||
|
const request: UserValidationRequest = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
apiKey: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.validateUserCredentials(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('API Key不能为空');
|
||||||
|
expect(mockFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝无效邮箱', async () => {
|
||||||
|
const request: UserValidationRequest = {
|
||||||
|
email: 'invalid-email',
|
||||||
|
apiKey: 'some-api-key',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.validateUserCredentials(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('邮箱格式无效');
|
||||||
|
expect(mockFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllUsers - 获取所有用户', () => {
|
||||||
|
it('应该成功获取用户列表', async () => {
|
||||||
|
// 模拟API响应
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
user_id: 1,
|
||||||
|
email: 'user1@example.com',
|
||||||
|
full_name: 'User One',
|
||||||
|
is_active: true,
|
||||||
|
is_admin: false,
|
||||||
|
is_bot: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user_id: 2,
|
||||||
|
email: 'user2@example.com',
|
||||||
|
full_name: 'User Two',
|
||||||
|
is_active: true,
|
||||||
|
is_admin: true,
|
||||||
|
is_bot: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const result = await service.getAllUsers();
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.users).toHaveLength(2);
|
||||||
|
expect(result.totalCount).toBe(2);
|
||||||
|
expect(result.users?.[0]).toEqual({
|
||||||
|
userId: 1,
|
||||||
|
email: 'user1@example.com',
|
||||||
|
fullName: 'User One',
|
||||||
|
isActive: true,
|
||||||
|
isAdmin: false,
|
||||||
|
isBot: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理空用户列表', async () => {
|
||||||
|
// 模拟API响应
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
members: [],
|
||||||
|
}),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const result = await service.getAllUsers();
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.users).toHaveLength(0);
|
||||||
|
expect(result.totalCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理API调用失败', async () => {
|
||||||
|
// 模拟API失败
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
status: 403,
|
||||||
|
statusText: 'Forbidden',
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const result = await service.getAllUsers();
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain('API调用失败');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
539
src/core/zulip/services/user_management.service.ts
Normal file
539
src/core/zulip/services/user_management.service.ts
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
/**
|
||||||
|
* Zulip用户管理服务
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 查询和验证Zulip用户信息
|
||||||
|
* - 检查用户是否存在
|
||||||
|
* - 获取用户详细信息
|
||||||
|
* - 验证用户凭据和权限
|
||||||
|
*
|
||||||
|
* 主要方法:
|
||||||
|
* - checkUserExists(): 检查用户是否存在
|
||||||
|
* - getUserInfo(): 获取用户详细信息
|
||||||
|
* - validateUserCredentials(): 验证用户凭据
|
||||||
|
* - getAllUsers(): 获取所有用户列表
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* - 用户登录时验证用户存在性
|
||||||
|
* - 获取用户基本信息
|
||||||
|
* - 验证用户权限和状态
|
||||||
|
* - 管理员查看用户列表
|
||||||
|
*
|
||||||
|
* @author angjustinl
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-01-06
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||||
|
import { IZulipConfigService } from '../interfaces/zulip-core.interfaces';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zulip API响应接口
|
||||||
|
*/
|
||||||
|
interface ZulipApiResponse {
|
||||||
|
result?: 'success' | 'error';
|
||||||
|
msg?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户信息接口
|
||||||
|
*/
|
||||||
|
interface ZulipUser {
|
||||||
|
user_id: number;
|
||||||
|
email: string;
|
||||||
|
full_name: string;
|
||||||
|
is_active: boolean;
|
||||||
|
is_admin: boolean;
|
||||||
|
is_owner: boolean;
|
||||||
|
is_bot: boolean;
|
||||||
|
date_joined: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户列表响应接口
|
||||||
|
*/
|
||||||
|
interface ZulipUsersResponse extends ZulipApiResponse {
|
||||||
|
members?: ZulipUser[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户查询请求接口
|
||||||
|
*/
|
||||||
|
export interface UserQueryRequest {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户信息响应接口
|
||||||
|
*/
|
||||||
|
export interface UserInfoResponse {
|
||||||
|
success: boolean;
|
||||||
|
userId?: number;
|
||||||
|
email?: string;
|
||||||
|
fullName?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
isBot?: boolean;
|
||||||
|
dateJoined?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户验证请求接口
|
||||||
|
*/
|
||||||
|
export interface UserValidationRequest {
|
||||||
|
email: string;
|
||||||
|
apiKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户验证响应接口
|
||||||
|
*/
|
||||||
|
export interface UserValidationResponse {
|
||||||
|
success: boolean;
|
||||||
|
isValid?: boolean;
|
||||||
|
userId?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户列表响应接口
|
||||||
|
*/
|
||||||
|
export interface UsersListResponse {
|
||||||
|
success: boolean;
|
||||||
|
users?: Array<{
|
||||||
|
userId: number;
|
||||||
|
email: string;
|
||||||
|
fullName: string;
|
||||||
|
isActive: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
|
isBot: boolean;
|
||||||
|
}>;
|
||||||
|
totalCount?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zulip用户管理服务类
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 查询和验证Zulip用户信息
|
||||||
|
* - 检查用户是否存在于Zulip服务器
|
||||||
|
* - 获取用户详细信息和权限状态
|
||||||
|
* - 提供用户管理相关的API接口
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class UserManagementService {
|
||||||
|
private readonly logger = new Logger(UserManagementService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject('ZULIP_CONFIG_SERVICE')
|
||||||
|
private readonly configService: IZulipConfigService,
|
||||||
|
) {
|
||||||
|
this.logger.log('UserManagementService初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否存在
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 通过Zulip API检查指定邮箱的用户是否存在
|
||||||
|
*
|
||||||
|
* 业务逻辑:
|
||||||
|
* 1. 获取所有用户列表
|
||||||
|
* 2. 在列表中查找指定邮箱
|
||||||
|
* 3. 返回用户存在性结果
|
||||||
|
*
|
||||||
|
* @param email 用户邮箱
|
||||||
|
* @returns Promise<boolean> 是否存在
|
||||||
|
*/
|
||||||
|
async checkUserExists(email: string): Promise<boolean> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
this.logger.log('开始检查用户是否存在', {
|
||||||
|
operation: 'checkUserExists',
|
||||||
|
email,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 验证邮箱格式
|
||||||
|
if (!email || !this.isValidEmail(email)) {
|
||||||
|
this.logger.warn('邮箱格式无效', {
|
||||||
|
operation: 'checkUserExists',
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取用户列表
|
||||||
|
const usersResult = await this.getAllUsers();
|
||||||
|
if (!usersResult.success) {
|
||||||
|
this.logger.warn('获取用户列表失败', {
|
||||||
|
operation: 'checkUserExists',
|
||||||
|
email,
|
||||||
|
error: usersResult.error,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查用户是否存在
|
||||||
|
const userExists = usersResult.users?.some(user =>
|
||||||
|
user.email.toLowerCase() === email.toLowerCase()
|
||||||
|
) || false;
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
this.logger.log('用户存在性检查完成', {
|
||||||
|
operation: 'checkUserExists',
|
||||||
|
email,
|
||||||
|
exists: userExists,
|
||||||
|
duration,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return userExists;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
this.logger.error('检查用户存在性失败', {
|
||||||
|
operation: 'checkUserExists',
|
||||||
|
email,
|
||||||
|
error: err.message,
|
||||||
|
duration,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}, err.stack);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户详细信息
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 根据邮箱获取用户的详细信息
|
||||||
|
*
|
||||||
|
* @param request 用户查询请求
|
||||||
|
* @returns Promise<UserInfoResponse>
|
||||||
|
*/
|
||||||
|
async getUserInfo(request: UserQueryRequest): Promise<UserInfoResponse> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
this.logger.log('开始获取用户信息', {
|
||||||
|
operation: 'getUserInfo',
|
||||||
|
email: request.email,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 验证请求参数
|
||||||
|
if (!request.email || !this.isValidEmail(request.email)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '邮箱格式无效',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取用户列表
|
||||||
|
const usersResult = await this.getAllUsers();
|
||||||
|
if (!usersResult.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: usersResult.error || '获取用户列表失败',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 查找指定用户
|
||||||
|
const user = usersResult.users?.find(u =>
|
||||||
|
u.email.toLowerCase() === request.email.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '用户不存在',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
this.logger.log('用户信息获取完成', {
|
||||||
|
operation: 'getUserInfo',
|
||||||
|
email: request.email,
|
||||||
|
userId: user.userId,
|
||||||
|
duration,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
userId: user.userId,
|
||||||
|
email: user.email,
|
||||||
|
fullName: user.fullName,
|
||||||
|
isActive: user.isActive,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
isBot: user.isBot,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
this.logger.error('获取用户信息失败', {
|
||||||
|
operation: 'getUserInfo',
|
||||||
|
email: request.email,
|
||||||
|
error: err.message,
|
||||||
|
duration,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}, err.stack);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '系统错误,请稍后重试',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户凭据
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 验证用户的API Key是否有效
|
||||||
|
*
|
||||||
|
* @param request 用户验证请求
|
||||||
|
* @returns Promise<UserValidationResponse>
|
||||||
|
*/
|
||||||
|
async validateUserCredentials(request: UserValidationRequest): Promise<UserValidationResponse> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
this.logger.log('开始验证用户凭据', {
|
||||||
|
operation: 'validateUserCredentials',
|
||||||
|
email: request.email,
|
||||||
|
hasApiKey: !!request.apiKey,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 验证请求参数
|
||||||
|
if (!request.email || !this.isValidEmail(request.email)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '邮箱格式无效',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.apiKey) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'API Key不能为空',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 使用用户的API Key测试连接
|
||||||
|
const isValid = await this.testUserApiKey(request.email, request.apiKey);
|
||||||
|
|
||||||
|
// 3. 如果API Key有效,获取用户ID
|
||||||
|
let userId = undefined;
|
||||||
|
if (isValid) {
|
||||||
|
const userInfo = await this.getUserInfo({ email: request.email });
|
||||||
|
if (userInfo.success) {
|
||||||
|
userId = userInfo.userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
this.logger.log('用户凭据验证完成', {
|
||||||
|
operation: 'validateUserCredentials',
|
||||||
|
email: request.email,
|
||||||
|
isValid,
|
||||||
|
userId,
|
||||||
|
duration,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
isValid,
|
||||||
|
userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
this.logger.error('验证用户凭据失败', {
|
||||||
|
operation: 'validateUserCredentials',
|
||||||
|
email: request.email,
|
||||||
|
error: err.message,
|
||||||
|
duration,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}, err.stack);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '系统错误,请稍后重试',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有用户列表
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 从Zulip服务器获取所有用户的列表
|
||||||
|
*
|
||||||
|
* @returns Promise<UsersListResponse>
|
||||||
|
*/
|
||||||
|
async getAllUsers(): Promise<UsersListResponse> {
|
||||||
|
this.logger.debug('开始获取用户列表', {
|
||||||
|
operation: 'getAllUsers',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取Zulip配置
|
||||||
|
const config = this.configService.getZulipConfig();
|
||||||
|
|
||||||
|
// 构建API URL
|
||||||
|
const apiUrl = `${config.zulipServerUrl}/api/v1/users`;
|
||||||
|
|
||||||
|
// 构建认证头
|
||||||
|
const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64');
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
this.logger.warn('获取用户列表失败', {
|
||||||
|
operation: 'getAllUsers',
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `API调用失败: ${response.status} ${response.statusText}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: ZulipUsersResponse = await response.json();
|
||||||
|
|
||||||
|
// 转换数据格式
|
||||||
|
const users = data.members?.map(user => ({
|
||||||
|
userId: user.user_id,
|
||||||
|
email: user.email,
|
||||||
|
fullName: user.full_name,
|
||||||
|
isActive: user.is_active,
|
||||||
|
isAdmin: user.is_admin,
|
||||||
|
isBot: user.is_bot,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
this.logger.debug('用户列表获取完成', {
|
||||||
|
operation: 'getAllUsers',
|
||||||
|
userCount: users.length,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
users,
|
||||||
|
totalCount: users.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
this.logger.error('获取用户列表异常', {
|
||||||
|
operation: 'getAllUsers',
|
||||||
|
error: err.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}, err.stack);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '系统错误,请稍后重试',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试用户API Key是否有效
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 使用用户的API Key测试是否能够成功调用Zulip API
|
||||||
|
*
|
||||||
|
* @param email 用户邮箱
|
||||||
|
* @param apiKey 用户API Key
|
||||||
|
* @returns Promise<boolean> 是否有效
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async testUserApiKey(email: string, apiKey: string): Promise<boolean> {
|
||||||
|
this.logger.debug('测试用户API Key', {
|
||||||
|
operation: 'testUserApiKey',
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取Zulip配置
|
||||||
|
const config = this.configService.getZulipConfig();
|
||||||
|
|
||||||
|
// 构建API URL - 使用获取用户自己信息的接口
|
||||||
|
const apiUrl = `${config.zulipServerUrl}/api/v1/users/me`;
|
||||||
|
|
||||||
|
// 使用用户的API Key构建认证头
|
||||||
|
const auth = Buffer.from(`${email}:${apiKey}`).toString('base64');
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isValid = response.ok;
|
||||||
|
|
||||||
|
this.logger.debug('API Key测试完成', {
|
||||||
|
operation: 'testUserApiKey',
|
||||||
|
email,
|
||||||
|
isValid,
|
||||||
|
status: response.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
this.logger.error('测试API Key异常', {
|
||||||
|
operation: 'testUserApiKey',
|
||||||
|
email,
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证邮箱格式
|
||||||
|
*
|
||||||
|
* @param email 邮箱地址
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private isValidEmail(email: string): boolean {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
188
src/core/zulip/services/user_registration.service.spec.ts
Normal file
188
src/core/zulip/services/user_registration.service.spec.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
/**
|
||||||
|
* Zulip用户注册服务测试
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 测试UserRegistrationService的核心功能
|
||||||
|
* - 测试用户注册流程和验证逻辑
|
||||||
|
* - 测试错误处理和边界情况
|
||||||
|
*
|
||||||
|
* @author angjustinl
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-01-06
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { UserRegistrationService, UserRegistrationRequest } from './user_registration.service';
|
||||||
|
import { IZulipConfigService } from '../interfaces/zulip-core.interfaces';
|
||||||
|
|
||||||
|
describe('UserRegistrationService', () => {
|
||||||
|
let service: UserRegistrationService;
|
||||||
|
let mockConfigService: jest.Mocked<IZulipConfigService>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// 创建模拟的配置服务
|
||||||
|
mockConfigService = {
|
||||||
|
getZulipConfig: jest.fn().mockReturnValue({
|
||||||
|
zulipServerUrl: 'https://test.zulip.com',
|
||||||
|
zulipBotEmail: 'bot@test.com',
|
||||||
|
zulipBotApiKey: 'test-api-key',
|
||||||
|
}),
|
||||||
|
getMapIdByStream: jest.fn(),
|
||||||
|
getStreamByMap: jest.fn(),
|
||||||
|
getMapConfig: jest.fn(),
|
||||||
|
hasMap: jest.fn(),
|
||||||
|
getAllMapIds: jest.fn(),
|
||||||
|
getMapConfigByStream: jest.fn(),
|
||||||
|
getAllStreams: jest.fn(),
|
||||||
|
hasStream: jest.fn(),
|
||||||
|
findObjectByTopic: jest.fn(),
|
||||||
|
getObjectsInMap: jest.fn(),
|
||||||
|
getTopicByObject: jest.fn(),
|
||||||
|
findNearbyObject: jest.fn(),
|
||||||
|
reloadConfig: jest.fn(),
|
||||||
|
validateConfig: jest.fn(),
|
||||||
|
getAllMapConfigs: jest.fn(),
|
||||||
|
getConfigStats: jest.fn(),
|
||||||
|
getConfigFilePath: jest.fn(),
|
||||||
|
configFileExists: jest.fn(),
|
||||||
|
enableConfigWatcher: jest.fn(),
|
||||||
|
disableConfigWatcher: jest.fn(),
|
||||||
|
isConfigWatcherEnabled: jest.fn(),
|
||||||
|
getFullConfiguration: jest.fn(),
|
||||||
|
updateConfigValue: jest.fn(),
|
||||||
|
exportMapConfig: jest.fn(),
|
||||||
|
} as jest.Mocked<IZulipConfigService>;
|
||||||
|
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
UserRegistrationService,
|
||||||
|
{
|
||||||
|
provide: 'ZULIP_CONFIG_SERVICE',
|
||||||
|
useValue: mockConfigService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<UserRegistrationService>(UserRegistrationService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确初始化服务', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('registerUser - 用户注册', () => {
|
||||||
|
it('应该成功注册有效用户', async () => {
|
||||||
|
const request: UserRegistrationRequest = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
fullName: 'Test User',
|
||||||
|
password: 'password123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.registerUser(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.email).toBe(request.email);
|
||||||
|
expect(result.userId).toBeDefined();
|
||||||
|
expect(result.apiKey).toBeDefined();
|
||||||
|
expect(result.error).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝无效邮箱', async () => {
|
||||||
|
const request: UserRegistrationRequest = {
|
||||||
|
email: 'invalid-email',
|
||||||
|
fullName: 'Test User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.registerUser(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain('邮箱格式无效');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝空邮箱', async () => {
|
||||||
|
const request: UserRegistrationRequest = {
|
||||||
|
email: '',
|
||||||
|
fullName: 'Test User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.registerUser(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain('邮箱不能为空');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝空用户名', async () => {
|
||||||
|
const request: UserRegistrationRequest = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
fullName: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.registerUser(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain('用户全名不能为空');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝过短的用户名', async () => {
|
||||||
|
const request: UserRegistrationRequest = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
fullName: 'A',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.registerUser(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain('用户全名至少需要2个字符');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝过长的用户名', async () => {
|
||||||
|
const request: UserRegistrationRequest = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
fullName: 'A'.repeat(101), // 101个字符
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.registerUser(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain('用户全名不能超过100个字符');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝过短的密码', async () => {
|
||||||
|
const request: UserRegistrationRequest = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
fullName: 'Test User',
|
||||||
|
password: '123', // 只有3个字符
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.registerUser(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain('密码至少需要6个字符');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该接受没有密码的注册', async () => {
|
||||||
|
const request: UserRegistrationRequest = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
fullName: 'Test User',
|
||||||
|
// 不提供密码
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.registerUser(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该拒绝过长的短名称', async () => {
|
||||||
|
const request: UserRegistrationRequest = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
fullName: 'Test User',
|
||||||
|
shortName: 'A'.repeat(51), // 51个字符
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await service.registerUser(request);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain('短名称不能超过50个字符');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
531
src/core/zulip/services/user_registration.service.ts
Normal file
531
src/core/zulip/services/user_registration.service.ts
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
/**
|
||||||
|
* Zulip用户管理服务
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 查询和验证Zulip用户信息
|
||||||
|
* - 检查用户是否存在
|
||||||
|
* - 获取用户详细信息
|
||||||
|
* - 管理用户API Key(如果有权限)
|
||||||
|
*
|
||||||
|
* 主要方法:
|
||||||
|
* - checkUserExists(): 检查用户是否存在
|
||||||
|
* - getUserInfo(): 获取用户详细信息
|
||||||
|
* - validateUserCredentials(): 验证用户凭据
|
||||||
|
* - getUserApiKey(): 获取用户API Key(需要管理员权限)
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* - 用户登录时验证用户存在性
|
||||||
|
* - 获取用户基本信息
|
||||||
|
* - 验证用户权限和状态
|
||||||
|
*
|
||||||
|
* @author angjustinl
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-01-06
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||||
|
import { IZulipConfigService } from '../interfaces/zulip-core.interfaces';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zulip API响应接口
|
||||||
|
*/
|
||||||
|
interface ZulipApiResponse {
|
||||||
|
result?: 'success' | 'error';
|
||||||
|
msg?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户列表响应接口
|
||||||
|
*/
|
||||||
|
interface ZulipUsersResponse extends ZulipApiResponse {
|
||||||
|
members?: Array<{
|
||||||
|
email: string;
|
||||||
|
user_id: number;
|
||||||
|
full_name: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建用户响应接口
|
||||||
|
*/
|
||||||
|
interface ZulipCreateUserResponse extends ZulipApiResponse {
|
||||||
|
user_id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Key响应接口
|
||||||
|
*/
|
||||||
|
interface ZulipApiKeyResponse extends ZulipApiResponse {
|
||||||
|
api_key?: string;
|
||||||
|
}
|
||||||
|
export interface UserRegistrationRequest {
|
||||||
|
email: string;
|
||||||
|
fullName: string;
|
||||||
|
password?: string;
|
||||||
|
shortName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户注册响应接口
|
||||||
|
*/
|
||||||
|
export interface UserRegistrationResponse {
|
||||||
|
success: boolean;
|
||||||
|
userId?: number;
|
||||||
|
email?: string;
|
||||||
|
apiKey?: string;
|
||||||
|
error?: string;
|
||||||
|
details?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zulip用户注册服务类
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 处理新用户在Zulip服务器上的注册
|
||||||
|
* - 验证用户信息的有效性
|
||||||
|
* - 与Zulip API交互创建用户账户
|
||||||
|
* - 管理注册流程和错误处理
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class UserRegistrationService {
|
||||||
|
private readonly logger = new Logger(UserRegistrationService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject('ZULIP_CONFIG_SERVICE')
|
||||||
|
private readonly configService: IZulipConfigService,
|
||||||
|
) {
|
||||||
|
this.logger.log('UserRegistrationService初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册新用户到Zulip服务器
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 在Zulip服务器上创建新用户账户
|
||||||
|
*
|
||||||
|
* 业务逻辑:
|
||||||
|
* 1. 验证用户注册信息
|
||||||
|
* 2. 检查用户是否已存在
|
||||||
|
* 3. 调用Zulip API创建用户
|
||||||
|
* 4. 获取用户API Key
|
||||||
|
* 5. 返回注册结果
|
||||||
|
*
|
||||||
|
* @param request 用户注册请求数据
|
||||||
|
* @returns Promise<UserRegistrationResponse>
|
||||||
|
*/
|
||||||
|
async registerUser(request: UserRegistrationRequest): Promise<UserRegistrationResponse> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
this.logger.log('开始注册Zulip用户', {
|
||||||
|
operation: 'registerUser',
|
||||||
|
email: request.email,
|
||||||
|
fullName: request.fullName,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 验证用户注册信息
|
||||||
|
const validationResult = this.validateUserInfo(request);
|
||||||
|
if (!validationResult.valid) {
|
||||||
|
this.logger.warn('用户注册信息验证失败', {
|
||||||
|
operation: 'registerUser',
|
||||||
|
email: request.email,
|
||||||
|
errors: validationResult.errors,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: validationResult.errors.join(', '),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 实现实际的Zulip用户注册逻辑
|
||||||
|
// 这里先返回模拟结果,后续步骤中实现真实的API调用
|
||||||
|
|
||||||
|
// 2. 检查用户是否已存在
|
||||||
|
const userExists = await this.checkUserExists(request.email);
|
||||||
|
if (userExists) {
|
||||||
|
this.logger.warn('用户注册失败:用户已存在', {
|
||||||
|
operation: 'registerUser',
|
||||||
|
email: request.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '用户已存在',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 调用Zulip API创建用户
|
||||||
|
const createResult = await this.createZulipUser(request);
|
||||||
|
if (!createResult.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: createResult.error || '创建用户失败',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 获取用户API Key(如果需要)
|
||||||
|
let apiKey = undefined;
|
||||||
|
if (createResult.userId) {
|
||||||
|
const apiKeyResult = await this.generateApiKey(createResult.userId, request.email);
|
||||||
|
if (apiKeyResult.success) {
|
||||||
|
apiKey = apiKeyResult.apiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
this.logger.log('Zulip用户注册完成(模拟)', {
|
||||||
|
operation: 'registerUser',
|
||||||
|
email: request.email,
|
||||||
|
duration,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
userId: createResult.userId,
|
||||||
|
email: request.email,
|
||||||
|
apiKey: apiKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
this.logger.error('Zulip用户注册失败', {
|
||||||
|
operation: 'registerUser',
|
||||||
|
email: request.email,
|
||||||
|
error: err.message,
|
||||||
|
duration,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}, err.stack);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '注册失败,请稍后重试',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户注册信息
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 验证用户提供的注册信息是否有效
|
||||||
|
*
|
||||||
|
* @param request 用户注册请求
|
||||||
|
* @returns {valid: boolean, errors: string[]} 验证结果
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private validateUserInfo(request: UserRegistrationRequest): {
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
} {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// 验证邮箱
|
||||||
|
if (!request.email || !request.email.trim()) {
|
||||||
|
errors.push('邮箱不能为空');
|
||||||
|
} else if (!this.isValidEmail(request.email)) {
|
||||||
|
errors.push('邮箱格式无效');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证全名
|
||||||
|
if (!request.fullName || !request.fullName.trim()) {
|
||||||
|
errors.push('用户全名不能为空');
|
||||||
|
} else if (request.fullName.trim().length < 2) {
|
||||||
|
errors.push('用户全名至少需要2个字符');
|
||||||
|
} else if (request.fullName.trim().length > 100) {
|
||||||
|
errors.push('用户全名不能超过100个字符');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码(如果提供)
|
||||||
|
if (request.password && request.password.length < 6) {
|
||||||
|
errors.push('密码至少需要6个字符');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证短名称(如果提供)
|
||||||
|
if (request.shortName && request.shortName.trim().length > 50) {
|
||||||
|
errors.push('短名称不能超过50个字符');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证邮箱格式
|
||||||
|
*
|
||||||
|
* @param email 邮箱地址
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private isValidEmail(email: string): boolean {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否已存在
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 通过Zulip API检查指定邮箱的用户是否已存在
|
||||||
|
*
|
||||||
|
* @param email 用户邮箱
|
||||||
|
* @returns Promise<boolean> 是否存在
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async checkUserExists(email: string): Promise<boolean> {
|
||||||
|
this.logger.debug('检查用户是否存在', {
|
||||||
|
operation: 'checkUserExists',
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取Zulip配置
|
||||||
|
const config = this.configService.getZulipConfig();
|
||||||
|
|
||||||
|
// 构建API URL
|
||||||
|
const apiUrl = `${config.zulipServerUrl}/api/v1/users`;
|
||||||
|
|
||||||
|
// 构建认证头
|
||||||
|
const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64');
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
this.logger.warn('获取用户列表失败', {
|
||||||
|
operation: 'checkUserExists',
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
});
|
||||||
|
return false; // 如果API调用失败,假设用户不存在
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: ZulipUsersResponse = await response.json();
|
||||||
|
|
||||||
|
// 检查用户是否在列表中
|
||||||
|
if (data.members && Array.isArray(data.members)) {
|
||||||
|
const userExists = data.members.some((user: any) =>
|
||||||
|
user.email && user.email.toLowerCase() === email.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.debug('用户存在性检查完成', {
|
||||||
|
operation: 'checkUserExists',
|
||||||
|
email,
|
||||||
|
exists: userExists,
|
||||||
|
});
|
||||||
|
|
||||||
|
return userExists;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
this.logger.error('检查用户存在性失败', {
|
||||||
|
operation: 'checkUserExists',
|
||||||
|
email,
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果检查失败,假设用户不存在,允许继续注册
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建Zulip用户
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 通过Zulip API创建新用户账户
|
||||||
|
*
|
||||||
|
* @param request 用户注册请求
|
||||||
|
* @returns Promise<{success: boolean, userId?: number, error?: string}>
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async createZulipUser(request: UserRegistrationRequest): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
userId?: number;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
this.logger.log('开始创建Zulip用户', {
|
||||||
|
operation: 'createZulipUser',
|
||||||
|
email: request.email,
|
||||||
|
fullName: request.fullName,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取Zulip配置
|
||||||
|
const config = this.configService.getZulipConfig();
|
||||||
|
|
||||||
|
// 构建API URL
|
||||||
|
const apiUrl = `${config.zulipServerUrl}/api/v1/users`;
|
||||||
|
|
||||||
|
// 构建认证头
|
||||||
|
const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64');
|
||||||
|
|
||||||
|
// 构建请求体
|
||||||
|
const requestBody = new URLSearchParams();
|
||||||
|
requestBody.append('email', request.email);
|
||||||
|
requestBody.append('full_name', request.fullName);
|
||||||
|
|
||||||
|
if (request.password) {
|
||||||
|
requestBody.append('password', request.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.shortName) {
|
||||||
|
requestBody.append('short_name', request.shortName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: requestBody.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: ZulipCreateUserResponse = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
this.logger.warn('Zulip用户创建失败', {
|
||||||
|
operation: 'createZulipUser',
|
||||||
|
email: request.email,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: data.msg || data.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: data.msg || data.message || '创建用户失败',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log('Zulip用户创建成功', {
|
||||||
|
operation: 'createZulipUser',
|
||||||
|
email: request.email,
|
||||||
|
userId: data.user_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
userId: data.user_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
this.logger.error('创建Zulip用户异常', {
|
||||||
|
operation: 'createZulipUser',
|
||||||
|
email: request.email,
|
||||||
|
error: err.message,
|
||||||
|
}, err.stack);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '系统错误,请稍后重试',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为用户生成API Key
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 为新创建的用户生成API Key,用于后续的Zulip API调用
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param email 用户邮箱
|
||||||
|
* @returns Promise<{success: boolean, apiKey?: string, error?: string}>
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async generateApiKey(userId: number, email: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
apiKey?: string;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
this.logger.log('开始生成用户API Key', {
|
||||||
|
operation: 'generateApiKey',
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取Zulip配置
|
||||||
|
const config = this.configService.getZulipConfig();
|
||||||
|
|
||||||
|
// 构建API URL
|
||||||
|
const apiUrl = `${config.zulipServerUrl}/api/v1/users/${userId}/api_key/regenerate`;
|
||||||
|
|
||||||
|
// 构建认证头
|
||||||
|
const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64');
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: ZulipApiKeyResponse = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
this.logger.warn('生成API Key失败', {
|
||||||
|
operation: 'generateApiKey',
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
error: data.msg || data.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: data.msg || data.message || '生成API Key失败',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log('API Key生成成功', {
|
||||||
|
operation: 'generateApiKey',
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
apiKey: data.api_key,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
this.logger.error('生成API Key异常', {
|
||||||
|
operation: 'generateApiKey',
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
error: err.message,
|
||||||
|
}, err.stack);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '系统错误,请稍后重试',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
196
test_zulip_registration.js
Normal file
196
test_zulip_registration.js
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* Zulip用户注册真实环境测试脚本
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 测试Zulip用户注册功能在真实环境下的表现
|
||||||
|
* - 验证API调用是否正常工作
|
||||||
|
* - 检查配置是否正确
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* node test_zulip_registration.js
|
||||||
|
*
|
||||||
|
* @author angjustinl
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-01-06
|
||||||
|
*/
|
||||||
|
|
||||||
|
const https = require('https');
|
||||||
|
const { URLSearchParams } = require('url');
|
||||||
|
|
||||||
|
// 配置信息
|
||||||
|
const config = {
|
||||||
|
zulipServerUrl: 'https://zulip.xinghangee.icu',
|
||||||
|
zulipBotEmail: 'angjustinl@mail.angforever.top',
|
||||||
|
zulipBotApiKey: 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否存在
|
||||||
|
*/
|
||||||
|
async function checkUserExists(email) {
|
||||||
|
console.log(`🔍 检查用户是否存在: ${email}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `${config.zulipServerUrl}/api/v1/users`;
|
||||||
|
const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64');
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.log(`❌ 获取用户列表失败: ${response.status} ${response.statusText}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(`📊 获取到 ${data.members?.length || 0} 个用户`);
|
||||||
|
|
||||||
|
if (data.members && Array.isArray(data.members)) {
|
||||||
|
const userExists = data.members.some(user =>
|
||||||
|
user.email && user.email.toLowerCase() === email.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ 用户存在性检查完成: ${userExists ? '存在' : '不存在'}`);
|
||||||
|
return userExists;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 检查用户存在性失败:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建测试用户
|
||||||
|
*/
|
||||||
|
async function createTestUser(email, fullName, password) {
|
||||||
|
console.log(`🚀 开始创建用户: ${email}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `${config.zulipServerUrl}/api/v1/users`;
|
||||||
|
const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64');
|
||||||
|
|
||||||
|
const requestBody = new URLSearchParams();
|
||||||
|
requestBody.append('email', email);
|
||||||
|
requestBody.append('full_name', fullName);
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
requestBody.append('password', password);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: requestBody.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.log(`❌ 用户创建失败: ${response.status} ${response.statusText}`);
|
||||||
|
console.log(`📝 错误信息: ${data.msg || data.message || '未知错误'}`);
|
||||||
|
return { success: false, error: data.msg || data.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ 用户创建成功! 用户ID: ${data.user_id}`);
|
||||||
|
return { success: true, userId: data.user_id };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 创建用户异常:`, error.message);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试连接
|
||||||
|
*/
|
||||||
|
async function testConnection() {
|
||||||
|
console.log('🔗 测试Zulip服务器连接...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `${config.zulipServerUrl}/api/v1/server_settings`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(`✅ 连接成功! 服务器版本: ${data.zulip_version || '未知'}`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.log(`❌ 连接失败: ${response.status} ${response.statusText}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 连接异常:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主测试函数
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
console.log('🎯 开始Zulip用户注册测试');
|
||||||
|
console.log('=' * 50);
|
||||||
|
|
||||||
|
// 1. 测试连接
|
||||||
|
const connected = await testConnection();
|
||||||
|
if (!connected) {
|
||||||
|
console.log('❌ 无法连接到Zulip服务器,测试终止');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 2. 生成测试用户信息
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const testEmail = `test_user_${timestamp}@example.com`;
|
||||||
|
const testFullName = `Test User ${timestamp}`;
|
||||||
|
const testPassword = 'test123456';
|
||||||
|
|
||||||
|
console.log(`📋 测试用户信息:`);
|
||||||
|
console.log(` 邮箱: ${testEmail}`);
|
||||||
|
console.log(` 姓名: ${testFullName}`);
|
||||||
|
console.log(` 密码: ${testPassword}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 3. 检查用户是否已存在
|
||||||
|
const userExists = await checkUserExists(testEmail);
|
||||||
|
if (userExists) {
|
||||||
|
console.log('⚠️ 用户已存在,跳过创建测试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 4. 创建用户
|
||||||
|
const createResult = await createTestUser(testEmail, testFullName, testPassword);
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('📊 测试结果:');
|
||||||
|
if (createResult.success) {
|
||||||
|
console.log('✅ 用户注册功能正常工作');
|
||||||
|
console.log(` 新用户ID: ${createResult.userId}`);
|
||||||
|
} else {
|
||||||
|
console.log('❌ 用户注册功能存在问题');
|
||||||
|
console.log(` 错误信息: ${createResult.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('🎉 测试完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('💥 测试过程中发生未处理的错误:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
275
test_zulip_user_management.js
Normal file
275
test_zulip_user_management.js
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
/**
|
||||||
|
* Zulip用户管理真实环境测试脚本
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 测试Zulip用户管理功能在真实环境下的表现
|
||||||
|
* - 验证用户查询、验证等API调用是否正常工作
|
||||||
|
* - 检查配置是否正确
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* node test_zulip_user_management.js
|
||||||
|
*
|
||||||
|
* @author angjustinl
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-01-06
|
||||||
|
*/
|
||||||
|
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
// 配置信息
|
||||||
|
const config = {
|
||||||
|
zulipServerUrl: 'https://zulip.xinghangee.icu',
|
||||||
|
zulipBotEmail: 'angjustinl@mail.angforever.top',
|
||||||
|
zulipBotApiKey: 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有用户列表
|
||||||
|
*/
|
||||||
|
async function getAllUsers() {
|
||||||
|
console.log('📋 获取所有用户列表...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `${config.zulipServerUrl}/api/v1/users`;
|
||||||
|
const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64');
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.log(`❌ 获取用户列表失败: ${response.status} ${response.statusText}`);
|
||||||
|
return { success: false, error: `${response.status} ${response.statusText}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const users = data.members?.map(user => ({
|
||||||
|
userId: user.user_id,
|
||||||
|
email: user.email,
|
||||||
|
fullName: user.full_name,
|
||||||
|
isActive: user.is_active,
|
||||||
|
isAdmin: user.is_admin,
|
||||||
|
isBot: user.is_bot,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
console.log(`✅ 成功获取 ${users.length} 个用户`);
|
||||||
|
|
||||||
|
// 显示前几个用户信息
|
||||||
|
console.log('👥 用户列表预览:');
|
||||||
|
users.slice(0, 5).forEach((user, index) => {
|
||||||
|
console.log(` ${index + 1}. ${user.fullName} (${user.email})`);
|
||||||
|
console.log(` ID: ${user.userId}, 活跃: ${user.isActive}, 管理员: ${user.isAdmin}, 机器人: ${user.isBot}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (users.length > 5) {
|
||||||
|
console.log(` ... 还有 ${users.length - 5} 个用户`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, users, totalCount: users.length };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 获取用户列表异常:`, error.message);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查指定用户是否存在
|
||||||
|
*/
|
||||||
|
async function checkUserExists(email) {
|
||||||
|
console.log(`🔍 检查用户是否存在: ${email}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const usersResult = await getAllUsers();
|
||||||
|
if (!usersResult.success) {
|
||||||
|
console.log(`❌ 无法获取用户列表: ${usersResult.error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userExists = usersResult.users.some(user =>
|
||||||
|
user.email.toLowerCase() === email.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ 用户存在性检查完成: ${userExists ? '存在' : '不存在'}`);
|
||||||
|
return userExists;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 检查用户存在性失败:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户详细信息
|
||||||
|
*/
|
||||||
|
async function getUserInfo(email) {
|
||||||
|
console.log(`📝 获取用户信息: ${email}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const usersResult = await getAllUsers();
|
||||||
|
if (!usersResult.success) {
|
||||||
|
console.log(`❌ 无法获取用户列表: ${usersResult.error}`);
|
||||||
|
return { success: false, error: usersResult.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = usersResult.users.find(u =>
|
||||||
|
u.email.toLowerCase() === email.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.log(`❌ 用户不存在: ${email}`);
|
||||||
|
return { success: false, error: '用户不存在' };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ 用户信息获取成功:`);
|
||||||
|
console.log(` 用户ID: ${user.userId}`);
|
||||||
|
console.log(` 邮箱: ${user.email}`);
|
||||||
|
console.log(` 姓名: ${user.fullName}`);
|
||||||
|
console.log(` 状态: ${user.isActive ? '活跃' : '非活跃'}`);
|
||||||
|
console.log(` 权限: ${user.isAdmin ? '管理员' : '普通用户'}`);
|
||||||
|
console.log(` 类型: ${user.isBot ? '机器人' : '真实用户'}`);
|
||||||
|
|
||||||
|
return { success: true, user };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 获取用户信息失败:`, error.message);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试用户API Key
|
||||||
|
*/
|
||||||
|
async function testUserApiKey(email, apiKey) {
|
||||||
|
console.log(`🔑 测试用户API Key: ${email}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `${config.zulipServerUrl}/api/v1/users/me`;
|
||||||
|
const auth = Buffer.from(`${email}:${apiKey}`).toString('base64');
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isValid = response.ok;
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(`✅ API Key有效! 用户信息:`);
|
||||||
|
console.log(` 用户ID: ${data.user_id}`);
|
||||||
|
console.log(` 邮箱: ${data.email}`);
|
||||||
|
console.log(` 姓名: ${data.full_name}`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ API Key无效: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 测试API Key异常:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试连接
|
||||||
|
*/
|
||||||
|
async function testConnection() {
|
||||||
|
console.log('🔗 测试Zulip服务器连接...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `${config.zulipServerUrl}/api/v1/server_settings`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(`✅ 连接成功! 服务器信息:`);
|
||||||
|
console.log(` 版本: ${data.zulip_version || '未知'}`);
|
||||||
|
console.log(` 服务器: ${data.realm_name || '未知'}`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.log(`❌ 连接失败: ${response.status} ${response.statusText}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 连接异常:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主测试函数
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
console.log('🎯 开始Zulip用户管理测试');
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
|
||||||
|
// 1. 测试连接
|
||||||
|
const connected = await testConnection();
|
||||||
|
if (!connected) {
|
||||||
|
console.log('❌ 无法连接到Zulip服务器,测试终止');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 2. 获取所有用户列表
|
||||||
|
const usersResult = await getAllUsers();
|
||||||
|
if (!usersResult.success) {
|
||||||
|
console.log('❌ 无法获取用户列表,测试终止');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 3. 测试用户存在性检查
|
||||||
|
const testEmails = [
|
||||||
|
'angjustinl@mail.angforever.top', // 应该存在
|
||||||
|
'nonexistent@example.com', // 应该不存在
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('🔍 测试用户存在性检查:');
|
||||||
|
for (const email of testEmails) {
|
||||||
|
const exists = await checkUserExists(email);
|
||||||
|
console.log(` ${email}: ${exists ? '✅ 存在' : '❌ 不存在'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 4. 测试获取用户信息
|
||||||
|
console.log('📝 测试获取用户信息:');
|
||||||
|
const existingEmail = 'angjustinl@mail.angforever.top';
|
||||||
|
const userInfoResult = await getUserInfo(existingEmail);
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 5. 测试API Key验证(如果有的话)
|
||||||
|
console.log('🔑 测试API Key验证:');
|
||||||
|
const testApiKey = 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8'; // 这是我们的测试API Key
|
||||||
|
const apiKeyValid = await testUserApiKey(existingEmail, testApiKey);
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('📊 测试结果总结:');
|
||||||
|
console.log(`✅ 服务器连接: 正常`);
|
||||||
|
console.log(`✅ 用户列表获取: 正常 (${usersResult.totalCount} 个用户)`);
|
||||||
|
console.log(`✅ 用户存在性检查: 正常`);
|
||||||
|
console.log(`✅ 用户信息获取: ${userInfoResult.success ? '正常' : '异常'}`);
|
||||||
|
console.log(`✅ API Key验证: ${apiKeyValid ? '正常' : '异常'}`);
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('🎉 用户管理功能测试完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('💥 测试过程中发生未处理的错误:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user