|
|
@@ -0,0 +1,298 @@
|
|
|
+import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
+import { testClient } from 'hono/testing';
|
|
|
+import { agoraApiRoutes } from '@/server/api';
|
|
|
+import { authMiddleware } from '@/server/middleware/auth.middleware';
|
|
|
+
|
|
|
+// Mock UI components
|
|
|
+vi.mock('@/client/components/ui/button', () => ({
|
|
|
+ Button: ({ children, onClick, disabled, variant, className }: any) => (
|
|
|
+ <button
|
|
|
+ onClick={onClick}
|
|
|
+ disabled={disabled}
|
|
|
+ data-variant={variant}
|
|
|
+ className={className}
|
|
|
+ >
|
|
|
+ {children}
|
|
|
+ </button>
|
|
|
+ )
|
|
|
+}));
|
|
|
+
|
|
|
+vi.mock('@/client/components/ui/card', () => ({
|
|
|
+ Card: ({ children, className }: any) => (
|
|
|
+ <div className={className}>{children}</div>
|
|
|
+ ),
|
|
|
+ CardHeader: ({ children }: any) => <div>{children}</div>,
|
|
|
+ CardTitle: ({ children }: any) => <h3>{children}</h3>,
|
|
|
+ CardDescription: ({ children }: any) => <p>{children}</p>,
|
|
|
+ CardContent: ({ children }: any) => <div>{children}</div>
|
|
|
+}));
|
|
|
+
|
|
|
+vi.mock('@/client/components/ui/badge', () => ({
|
|
|
+ Badge: ({ children, variant, className }: any) => (
|
|
|
+ <span data-variant={variant} className={className}>{children}</span>
|
|
|
+ )
|
|
|
+}));
|
|
|
+
|
|
|
+vi.mock('@/client/components/ui/alert', () => ({
|
|
|
+ Alert: ({ children }: any) => <div role="alert">{children}</div>,
|
|
|
+ AlertDescription: ({ children }: any) => <div>{children}</div>
|
|
|
+}));
|
|
|
+
|
|
|
+// Mock Lucide icons
|
|
|
+vi.mock('lucide-react', () => ({
|
|
|
+ Mic: () => <span>Mic</span>,
|
|
|
+ MicOff: () => <span>MicOff</span>,
|
|
|
+ Play: () => <span>Play</span>,
|
|
|
+ Square: () => <span>Square</span>,
|
|
|
+ Trash2: () => <span>Trash2</span>,
|
|
|
+ Wifi: () => <span>Wifi</span>,
|
|
|
+ WifiOff: () => <span>WifiOff</span>
|
|
|
+}));
|
|
|
+
|
|
|
+// Mock auth middleware
|
|
|
+vi.mock('@/server/middleware/auth.middleware', () => ({
|
|
|
+ authMiddleware: vi.fn()
|
|
|
+}));
|
|
|
+
|
|
|
+// Mock用户数据
|
|
|
+const mockUser = {
|
|
|
+ id: 1,
|
|
|
+ username: 'testuser',
|
|
|
+ password: 'password123',
|
|
|
+ phone: null,
|
|
|
+ email: 'test@example.com',
|
|
|
+ nickname: null,
|
|
|
+ name: null,
|
|
|
+ avatarFileId: null,
|
|
|
+ avatarFile: null,
|
|
|
+ isDisabled: 0,
|
|
|
+ isDeleted: 0,
|
|
|
+ roles: [],
|
|
|
+ createdAt: new Date(),
|
|
|
+ updatedAt: new Date()
|
|
|
+};
|
|
|
+
|
|
|
+// 只在有真实Agora配置时运行这些测试
|
|
|
+const hasRealAgoraConfig = process.env.AGORA_APP_ID && process.env.AGORA_APP_SECRET;
|
|
|
+
|
|
|
+
|
|
|
+describe('AgoraSTTComponent 真实API集成测试', () => {
|
|
|
+ let client: ReturnType<typeof testClient<typeof agoraApiRoutes>>['api']['v1'];
|
|
|
+
|
|
|
+ beforeEach(() => {
|
|
|
+ // Mock auth middleware
|
|
|
+ vi.mocked(authMiddleware).mockImplementation(async (c, next) => {
|
|
|
+ const authHeader = c.req.header('Authorization');
|
|
|
+ if (!authHeader) {
|
|
|
+ return c.json({ message: 'Authorization header missing' }, 401);
|
|
|
+ }
|
|
|
+ c.set('user', mockUser);
|
|
|
+ await next();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 创建测试客户端
|
|
|
+ client = testClient(agoraApiRoutes).api.v1;
|
|
|
+
|
|
|
+ // 设置环境变量
|
|
|
+ process.env.AGORA_APP_ID = process.env.AGORA_APP_ID || 'test-app-id';
|
|
|
+ process.env.AGORA_APP_SECRET = process.env.AGORA_APP_SECRET || 'test-app-secret';
|
|
|
+ process.env.AGORA_TOKEN_EXPIRY = '3600';
|
|
|
+ });
|
|
|
+
|
|
|
+ afterEach(() => {
|
|
|
+ vi.clearAllMocks();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 跳过测试如果没有真实配置
|
|
|
+ const runIfRealConfig = hasRealAgoraConfig ? test : test.skip;
|
|
|
+
|
|
|
+ runIfRealConfig('组件应该能够调用真实Token API', async () => {
|
|
|
+ // 模拟组件调用Token API
|
|
|
+ const response = await client.agora.token.$get({
|
|
|
+ query: { type: 'rtc', channel: 'test-channel' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': 'Bearer test-token'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+
|
|
|
+ // 验证真实Token格式
|
|
|
+ if ('token' in data) {
|
|
|
+ expect(data).toHaveProperty('token');
|
|
|
+ expect(data).toHaveProperty('type');
|
|
|
+ expect(data).toHaveProperty('expiresAt');
|
|
|
+ expect(data).toHaveProperty('expiresIn');
|
|
|
+ expect(data).toHaveProperty('generatedAt');
|
|
|
+
|
|
|
+ // 验证Token类型
|
|
|
+ expect(data.type).toBe('rtc');
|
|
|
+
|
|
|
+ // 验证Token格式
|
|
|
+ expect(data.token).toMatch(/^[A-Za-z0-9+/=]+$/);
|
|
|
+ expect(data.token.length).toBeGreaterThan(20);
|
|
|
+
|
|
|
+ // 验证时间戳格式
|
|
|
+ expect(data.expiresAt).toBeGreaterThan(data.generatedAt);
|
|
|
+ expect(data.expiresIn).toBeGreaterThan(0);
|
|
|
+ expect(data.expiresAt - data.generatedAt).toBe(data.expiresIn);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ runIfRealConfig('Token API错误处理测试', async () => {
|
|
|
+ // 测试缺少必需参数
|
|
|
+ const response = await client.agora.token.$get({
|
|
|
+ query: { type: 'rtc' } // 缺少channel参数
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': 'Bearer test-token'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(400);
|
|
|
+
|
|
|
+ const errorData = await response.json();
|
|
|
+
|
|
|
+ if ('message' in errorData) {
|
|
|
+ expect(errorData.message).toContain('RTC Token需要提供channel参数');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ runIfRealConfig('不同Token类型测试', async () => {
|
|
|
+ // 测试RTC Token
|
|
|
+ const rtcResponse = await client.agora.token.$get({
|
|
|
+ query: { type: 'rtc', channel: 'test-channel-rtc' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': 'Bearer test-token'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(rtcResponse.status).toBe(200);
|
|
|
+
|
|
|
+ const rtcData = await rtcResponse.json();
|
|
|
+ if ('token' in rtcData) {
|
|
|
+ expect(rtcData.type).toBe('rtc');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 测试RTM Token
|
|
|
+ const rtmResponse = await client.agora.token.$get({
|
|
|
+ query: { type: 'rtm', userId: 'test-user-rtm' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': 'Bearer test-token'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(rtmResponse.status).toBe(200);
|
|
|
+
|
|
|
+ const rtmData = await rtmResponse.json();
|
|
|
+ if ('token' in rtmData) {
|
|
|
+ expect(rtmData.type).toBe('rtm');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ runIfRealConfig('Token有效期验证', async () => {
|
|
|
+ const response = await client.agora.token.$get({
|
|
|
+ query: { type: 'rtc', channel: 'test-channel-validity' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': 'Bearer test-token'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+
|
|
|
+ if ('token' in data) {
|
|
|
+ // 验证Token在有效期内
|
|
|
+ const currentTime = Math.floor(Date.now() / 1000);
|
|
|
+ expect(data.expiresAt).toBeGreaterThan(currentTime);
|
|
|
+ expect(data.expiresAt).toBeLessThanOrEqual(currentTime + 3600); // 默认1小时有效期
|
|
|
+
|
|
|
+ // 验证生成时间在当前时间之前或等于
|
|
|
+ expect(data.generatedAt).toBeLessThanOrEqual(currentTime);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 组件集成测试 - 模拟组件使用真实API
|
|
|
+ runIfRealConfig('组件集成Token获取流程', async () => {
|
|
|
+ // 模拟组件调用Token API的过程
|
|
|
+ const fetchToken = async (type: 'rtc' | 'rtm', channel?: string, userId?: string) => {
|
|
|
+ const query: any = { type };
|
|
|
+
|
|
|
+ if (type === 'rtc' && channel) {
|
|
|
+ query.channel = channel;
|
|
|
+ } else if (type === 'rtm' && userId) {
|
|
|
+ query.userId = userId;
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await client.agora.token.$get({ query }, {
|
|
|
+ headers: { 'Authorization': 'Bearer test-token' }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`Token API returned ${response.status}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+ return data.token;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 测试组件会调用的Token获取
|
|
|
+ const token = await fetchToken('rtc', 'test-integration-channel');
|
|
|
+ expect(token).toBeTruthy();
|
|
|
+ expect(typeof token).toBe('string');
|
|
|
+ });
|
|
|
+
|
|
|
+ // 总是运行的测试(不依赖真实配置)
|
|
|
+ test('API认证保护测试', async () => {
|
|
|
+ // 测试未认证访问
|
|
|
+ const response = await client.agora.token.$get({
|
|
|
+ query: { type: 'rtc', channel: 'test-channel' }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(401);
|
|
|
+
|
|
|
+ const errorData = await response.json();
|
|
|
+ if ('message' in errorData) {
|
|
|
+ expect(errorData.message).toContain('Authorization header missing');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ test('无效Token类型参数验证', async () => {
|
|
|
+ const response = await client.agora.token.$get({
|
|
|
+ query: { type: 'invalid-type' as any, channel: 'test-channel' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': 'Bearer test-token'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 由于Zod验证,应该返回400错误
|
|
|
+ expect(response.status).toBe(400);
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+// 配置状态检查
|
|
|
+describe('AgoraSTTComponent 配置状态检查', () => {
|
|
|
+ test('检查Agora配置状态', () => {
|
|
|
+ if (!hasRealAgoraConfig) {
|
|
|
+ console.warn('⚠️ 没有真实的Agora配置,前端真实API集成测试将被跳过');
|
|
|
+ console.warn(' 请设置AGORA_APP_ID和AGORA_APP_SECRET环境变量来启用真实API测试');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 这个测试总是通过,用于提供信息
|
|
|
+ expect(true).toBe(true);
|
|
|
+ });
|
|
|
+});
|