mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: basic api server
This commit is contained in:
@@ -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)以避免与开发服务器冲突
|
||||
- 某些测试可能需要先创建数据才能测试查询功能
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createClient } from '../src/client'
|
||||
|
||||
export const getTestClient = () => {
|
||||
return createClient()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user