feat: basic api server

This commit is contained in:
Acbox
2026-01-10 20:17:02 +08:00
parent 28aa28e5bb
commit e60c0bb0d7
31 changed files with 1421 additions and 7 deletions
+39
View File
@@ -0,0 +1,39 @@
# API Tests
这个目录包含了使用 Elysia Eden Client 编写的 API 测试。
## 测试文件
- `setup.ts` - 测试设置文件,配置测试服务器和 eden client
- `memory.test.ts` - Memory API 测试
- `memory-message.test.ts` - Memory Message API 测试
- `model.test.ts` - Model API 测试
- `settings.test.ts` - Settings API 测试
## 运行测试
从项目根目录运行:
```bash
pnpm test
```
或者只运行 API 包的测试:
```bash
cd packages/api
pnpm test
```
## 测试说明
测试使用 vitest 作为测试框架,并使用 Elysia Eden Client (treaty) 来测试 API 端点。
测试服务器会在测试开始前启动(端口 7003),测试结束后自动关闭。
## 注意事项
- 确保数据库已配置并运行(某些测试可能需要数据库连接)
- 测试使用独立的测试端口(7003)以避免与开发服务器冲突
- 某些测试可能需要先创建数据才能测试查询功能
+80
View File
@@ -0,0 +1,80 @@
import { describe, it, expect } from 'vitest'
import { getTestClient } from './setup'
describe('Memory Message API', () => {
const client = getTestClient()
describe('GET /memory/message', () => {
it('should get memory messages successfully', async () => {
const response = await client.memory.message.get({
query: {
userId: 'test-user-123',
limit: 10,
page: 1,
},
})
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
expect(response.data?.units).toBeDefined()
})
it('should use default limit and page when not provided', async () => {
const response = await client.memory.message.get({
query: {
userId: 'test-user-123',
limit: 10,
page: 1,
},
})
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
})
it('should return error for missing userId', async () => {
const response = await client.memory.message.get({
// @ts-expect-error - Testing invalid input
query: {
limit: 10,
page: 1,
// missing userId
},
})
expect([400, 422]).toContain(response.status)
})
})
describe('GET /memory/message/filter', () => {
it('should filter memory messages successfully', async () => {
const response = await client.memory.message.filter.get({
query: {
userId: 'test-user-123',
from: new Date('2024-01-01') as unknown as string,
to: new Date('2024-12-31') as unknown as string,
},
})
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
expect(response.data?.units).toBeDefined()
})
it('should return error for missing required fields', async () => {
const response = await client.memory.message.filter.get({
// @ts-expect-error - Testing invalid input
query: {
userId: 'test-user-123',
// missing from and to
},
})
expect([400, 422]).toContain(response.status)
})
})
})
+81
View File
@@ -0,0 +1,81 @@
import { describe, it, expect } from 'vitest'
import { getTestClient } from './setup'
describe('Memory API', () => {
const client = getTestClient()
describe('POST /memory', () => {
it('should add memory successfully', async () => {
const memoryData = {
messages: [
{ role: 'user', content: 'Hello, this is a test message' },
{ role: 'assistant', content: 'Hello! How can I help you?' },
],
timestamp: new Date(),
user: 'test-user-123',
}
const response = await client.memory.post(memoryData)
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
console.log(response.data?.error)
expect(response.data?.success).toBe(true)
expect(response.data?.data).toBeDefined()
})
it('should return error for invalid memory data', async () => {
const invalidData = {
messages: [],
// missing timestamp and user
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response = await client.memory.post(invalidData as any)
// Elysia 会返回 400 或 422 对于验证错误
expect([400, 422]).toContain(response.status)
})
})
describe('GET /memory/search', () => {
it('should search memory successfully', async () => {
const response = await client.memory.search.get({
query: {
query: 'test search',
userId: 'test-user-123',
},
})
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
expect(Array.isArray(response.data?.data)).toBe(true)
})
it('should return error for missing query', async () => {
const response = await client.memory.search.get({
// @ts-expect-error - Testing invalid input
query: {
userId: 'test-user-123',
// missing query
},
})
expect([400, 422]).toContain(response.status)
})
it('should return error for missing userId', async () => {
const response = await client.memory.search.get({
// @ts-expect-error - Testing invalid input
query: {
query: 'test search',
// missing userId
},
})
expect([400, 422]).toContain(response.status)
})
})
})
+238
View File
@@ -0,0 +1,238 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { getTestClient } from './setup'
describe('Model API', () => {
const client = getTestClient()
let createdModelId: string | null = null
describe('GET /model', () => {
it('should get all models successfully', async () => {
const response = await client.model.get()
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
expect(Array.isArray(response.data?.data)).toBe(true)
})
})
describe('POST /model', () => {
it('should create a chat model successfully', async () => {
const modelData = {
modelId: 'test-chat-model',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'test-api-key',
clientType: 'openai',
name: 'Test Chat Model',
type: 'chat' as const,
}
const response = await client.model.post(modelData)
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
expect(response.data?.data).toBeDefined()
if (response.data?.data?.id) {
createdModelId = response.data.data.id
}
})
it('should create an embedding model successfully', async () => {
const modelData = {
modelId: 'test-embedding-model',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'test-api-key',
clientType: 'openai',
name: 'Test Embedding Model',
type: 'embedding' as const,
dimensions: 1536,
}
const response = await client.model.post(modelData)
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
expect(response.data?.data).toBeDefined()
})
it('should return error for invalid model data', async () => {
const invalidData = {
// missing required fields
name: 'Invalid Model',
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response = await client.model.post(invalidData as any)
expect([400, 422]).toContain(response.status)
})
it('should return error for embedding model without dimensions', async () => {
const invalidData = {
modelId: 'test-embedding-model',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'test-api-key',
clientType: 'openai',
type: 'embedding' as const,
// missing dimensions
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response = await client.model.post(invalidData as any)
expect([400, 422]).toContain(response.status)
})
})
describe('GET /model/:id', () => {
it('should get model by id successfully', async () => {
if (!createdModelId) {
// 先创建一个模型
const createResponse = await client.model.post({
modelId: 'test-get-model',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'test-api-key',
clientType: 'openai',
type: 'chat' as const,
})
if (createResponse.data?.data?.id) {
createdModelId = createResponse.data.data.id
}
}
if (createdModelId) {
const response = await client.model({
id: createdModelId,
}).get()
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
expect(response.data?.data).toBeDefined()
}
})
it('should return error for non-existent model', async () => {
const response = await client.model({
id: 'non-existent-id',
}).get()
expect(response.status).toBe(200) // API 返回 200 但 success: false
expect(response.data?.success).toBe(false)
expect(response.data?.error).toBeDefined()
})
})
describe('PUT /model/:id', () => {
it('should update model successfully', async () => {
if (!createdModelId) {
const createResponse = await client.model.post({
modelId: 'test-update-model',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'test-api-key',
clientType: 'openai',
type: 'chat' as const,
})
if (createResponse.data?.data?.id) {
createdModelId = createResponse.data.data.id
}
}
if (createdModelId) {
const updateData = {
modelId: 'test-updated-model',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'updated-api-key',
clientType: 'openai',
name: 'Updated Model',
type: 'chat' as const,
}
const response = await client.model[createdModelId].put(updateData)
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
expect(response.data?.data).toBeDefined()
}
})
})
describe('DELETE /model/:id', () => {
it('should delete model successfully', async () => {
// 先创建一个模型用于删除
const createResponse = await client.model.post({
modelId: 'test-delete-model',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'test-api-key',
clientType: 'openai',
type: 'chat' as const,
})
const modelId = createResponse.data?.data?.id
if (modelId) {
const response = await client.model[modelId].delete()
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
}
})
})
describe('GET /model/chat/default', () => {
it('should get default chat model successfully', async () => {
const response = await client.model.chat.default.get({
query: {
userId: 'test-user-123',
},
})
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
// 可能返回 success: false 如果没有设置默认模型
expect(response.data?.success !== undefined).toBe(true)
})
it('should return error for missing userId', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response = await client.model.chat.default.get({ query: {} } as any)
expect([400, 422]).toContain(response.status)
})
})
describe('GET /model/summary/default', () => {
it('should get default summary model successfully', async () => {
const response = await client.model.summary.default.get({
query: {
userId: 'test-user-123',
},
})
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success !== undefined).toBe(true)
})
})
describe('GET /model/embedding/default', () => {
it('should get default embedding model successfully', async () => {
const response = await client.model.embedding.default.get({
query: {
userId: 'test-user-123',
},
})
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success !== undefined).toBe(true)
})
})
})
+155
View File
@@ -0,0 +1,155 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { getTestClient } from './setup'
describe('Settings API', () => {
const client = getTestClient()
const testUserId = 'test-user-settings-123'
describe('GET /settings/:userId', () => {
it('should get settings successfully', async () => {
const response = await client.settings({
userId: testUserId,
}).get()
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
// 可能返回 success: false 如果设置不存在
expect(response.data?.success !== undefined).toBe(true)
})
})
describe('POST /settings', () => {
it('should create settings successfully', async () => {
const settingsData = {
userId: testUserId,
defaultChatModel: null,
defaultEmbeddingModel: null,
defaultSummaryModel: null,
}
const response = await client.settings.post(settingsData)
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
expect(response.data?.data).toBeDefined()
})
it('should create settings with model IDs', async () => {
const settingsData = {
userId: `${testUserId}-with-models`,
defaultChatModel: '00000000-0000-0000-0000-000000000001',
defaultEmbeddingModel: '00000000-0000-0000-0000-000000000002',
defaultSummaryModel: '00000000-0000-0000-0000-000000000003',
}
const response = await client.settings.post(settingsData)
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
})
it('should return error for invalid UUID format', async () => {
const invalidData = {
userId: testUserId,
defaultChatModel: 'invalid-uuid',
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response = await client.settings.post(invalidData as any)
expect([400, 422]).toContain(response.status)
})
it('should return error for missing userId', async () => {
const invalidData = {
defaultChatModel: null,
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response = await client.settings.post(invalidData as any)
expect([400, 422]).toContain(response.status)
})
})
describe('PUT /settings/:userId', () => {
it('should update settings successfully', async () => {
// 先创建设置
await client.settings.post({
userId: `${testUserId}-update`,
defaultChatModel: null,
})
const updateData = {
defaultChatModel: '00000000-0000-0000-0000-000000000001',
defaultEmbeddingModel: '00000000-0000-0000-0000-000000000002',
defaultSummaryModel: null,
}
const response = await client.settings({
userId: `${testUserId}-update`,
}).put(updateData)
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
expect(response.data?.data).toBeDefined()
})
it('should return error for non-existent settings', async () => {
const updateData = {
defaultChatModel: '00000000-0000-0000-0000-000000000001',
}
const response = await client.settings({
userId: 'non-existent-user',
}).put(updateData)
expect(response.status).toBe(200) // API 返回 200 但 success: false
expect(response.data?.success).toBe(false)
expect(response.data?.error).toBeDefined()
})
})
describe('PATCH /settings', () => {
it('should upsert settings successfully', async () => {
const settingsData = {
userId: `${testUserId}-upsert`,
defaultChatModel: '00000000-0000-0000-0000-000000000001',
defaultEmbeddingModel: null,
defaultSummaryModel: null,
}
const response = await client.settings.patch(settingsData)
expect(response.status).toBe(200)
expect(response.data).toBeDefined()
expect(response.data?.success).toBe(true)
expect(response.data?.data).toBeDefined()
})
it('should update existing settings on upsert', async () => {
const userId = `${testUserId}-upsert-update`
// 先创建
await client.settings.post({
userId,
defaultChatModel: null,
})
// 然后 upsert
const upsertData = {
userId,
defaultChatModel: '00000000-0000-0000-0000-000000000001',
}
const response = await client.settings.patch(upsertData)
expect(response.status).toBe(200)
expect(response.data?.success).toBe(true)
})
})
})
+7
View File
@@ -0,0 +1,7 @@
import { createClient } from '../src/client'
export const getTestClient = () => {
return createClient()
}