feat(wasm-go): add wasm go plugin unit test and ci workflow (#2809)

This commit is contained in:
Jingze
2025-08-28 20:02:03 +08:00
committed by GitHub
parent 3e0a5f02a7
commit a00b810be5
138 changed files with 27695 additions and 313 deletions

View File

@@ -0,0 +1,892 @@
// Copyright (c) 2024 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"encoding/json"
"testing"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/higress-group/wasm-go/pkg/test"
"github.com/santhosh-tekuri/jsonschema"
"github.com/stretchr/testify/require"
)
// 测试配置:基础配置
var basicConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"serviceName": "ai-service",
"serviceDomain": "api.openai.com",
"servicePort": 443,
"servicePath": "/v1/chat/completions",
"apiKey": "sk-test123",
"serviceTimeout": 30000,
"maxRetry": 3,
"contentPath": "choices.0.message.content",
"enableContentDisposition": true,
// 添加一个简单的JSON Schema避免编译失败
"jsonSchema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"content": map[string]interface{}{
"type": "string",
},
},
},
})
return data
}()
// 测试配置使用serviceUrl的配置
var serviceUrlConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"serviceName": "ai-service",
"serviceUrl": "https://api.openai.com/v1/chat/completions",
"apiKey": "sk-test456",
"serviceTimeout": 50000,
"maxRetry": 5,
"contentPath": "choices.0.message.content",
"enableContentDisposition": false,
// 添加一个简单的JSON Schema避免编译失败
"jsonSchema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"content": map[string]interface{}{
"type": "string",
},
},
},
})
return data
}()
// 测试配置包含JSON Schema的配置
var jsonSchemaConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"serviceName": "ai-service",
"serviceDomain": "api.openai.com",
"servicePort": 443,
"apiKey": "sk-test789",
"jsonSchema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string",
},
"age": map[string]interface{}{
"type": "integer",
},
},
"required": []string{"name"},
},
"enableSwagger": true,
"enableOas3": false,
})
return data
}()
// 测试配置启用OAS3的配置
var oas3Config = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"serviceName": "ai-service",
"serviceDomain": "api.openai.com",
"servicePort": 443,
"apiKey": "sk-test101",
"jsonSchema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"title": map[string]interface{}{
"type": "string",
},
"content": map[string]interface{}{
"type": "string",
},
},
},
"enableSwagger": false,
"enableOas3": true,
})
return data
}()
// 测试配置无效的JSON Schema配置
var invalidJsonSchemaConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"serviceName": "ai-service",
"serviceDomain": "api.openai.com",
"servicePort": 443,
"apiKey": "sk-test303",
"jsonSchema": "invalid-schema",
})
return data
}()
// 测试配置:缺少必需字段的配置
var missingRequiredConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"apiKey": "sk-test404",
"serviceTimeout": 30000,
})
return data
}()
func TestParseConfig(t *testing.T) {
test.RunGoTest(t, func(t *testing.T) {
// 测试基础配置解析
t.Run("basic config", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
config, err := host.GetMatchConfig()
require.NoError(t, err)
require.NotNil(t, config)
pluginConfig := config.(*PluginConfig)
require.Equal(t, "ai-service", pluginConfig.serviceName)
require.Equal(t, "api.openai.com", pluginConfig.serviceDomain)
require.Equal(t, 443, pluginConfig.servicePort)
require.Equal(t, "/v1/chat/completions", pluginConfig.servicePath)
require.Equal(t, "sk-test123", pluginConfig.apiKey)
require.Equal(t, 30000, pluginConfig.serviceTimeout)
require.Equal(t, 3, pluginConfig.maxRetry)
require.Equal(t, "choices.0.message.content", pluginConfig.contentPath)
require.True(t, pluginConfig.enableContentDisposition)
require.NotNil(t, pluginConfig.jsonSchema)
require.Equal(t, jsonschema.Draft7, pluginConfig.draft)
require.True(t, pluginConfig.enableJsonSchemaValidation)
})
// 测试使用serviceUrl的配置解析
t.Run("serviceUrl config", func(t *testing.T) {
host, status := test.NewTestHost(serviceUrlConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
config, err := host.GetMatchConfig()
require.NoError(t, err)
require.NotNil(t, config)
pluginConfig := config.(*PluginConfig)
require.Equal(t, "ai-service", pluginConfig.serviceName)
require.Equal(t, "api.openai.com", pluginConfig.serviceDomain)
require.Equal(t, 443, pluginConfig.servicePort)
require.Equal(t, "/v1/chat/completions", pluginConfig.servicePath)
require.Equal(t, "sk-test456", pluginConfig.apiKey)
require.Equal(t, 50000, pluginConfig.serviceTimeout)
require.Equal(t, 5, pluginConfig.maxRetry)
require.False(t, pluginConfig.enableContentDisposition)
})
// 测试包含JSON Schema的配置解析
t.Run("jsonSchema config", func(t *testing.T) {
host, status := test.NewTestHost(jsonSchemaConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
config, err := host.GetMatchConfig()
require.NoError(t, err)
require.NotNil(t, config)
pluginConfig := config.(*PluginConfig)
require.NotNil(t, pluginConfig.jsonSchema)
require.Equal(t, jsonschema.Draft4, pluginConfig.draft)
require.True(t, pluginConfig.enableJsonSchemaValidation)
require.NotNil(t, pluginConfig.compile)
})
// 测试启用OAS3的配置解析
t.Run("oas3 config", func(t *testing.T) {
host, status := test.NewTestHost(oas3Config)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
config, err := host.GetMatchConfig()
require.NoError(t, err)
require.NotNil(t, config)
pluginConfig := config.(*PluginConfig)
require.Equal(t, jsonschema.Draft7, pluginConfig.draft)
require.True(t, pluginConfig.enableJsonSchemaValidation)
})
// 测试无效的JSON Schema配置
t.Run("invalid jsonSchema config", func(t *testing.T) {
host, status := test.NewTestHost(invalidJsonSchemaConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
config, err := host.GetMatchConfig()
require.NoError(t, err)
require.NotNil(t, config)
pluginConfig := config.(*PluginConfig)
// 根据插件的实际行为无效的JSON Schema会导致编译失败
require.Equal(t, uint32(JSON_SCHEMA_COMPILE_FAILED_CODE), pluginConfig.rejectStruct.RejectCode)
})
// 测试缺少必需字段的配置
t.Run("missing required config", func(t *testing.T) {
host, status := test.NewTestHost(missingRequiredConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
config, _ := host.GetMatchConfig()
require.NotNil(t, config)
pluginConfig := config.(*PluginConfig)
// 根据插件的实际行为缺少serviceDomain会导致JSON Schema编译失败
require.Equal(t, uint32(JSON_SCHEMA_COMPILE_FAILED_CODE), pluginConfig.rejectStruct.RejectCode)
require.Contains(t, pluginConfig.rejectStruct.RejectMsg, "Json Schema compile failed")
})
})
}
func TestOnHttpRequestHeaders(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
// 测试正常请求头处理
t.Run("normal request headers", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置请求头
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Authorization", "Bearer sk-user123"},
{"Content-Type", "application/json"},
{"Content-Length", "100"},
})
// 应该返回ActionContinue
require.Equal(t, types.ActionContinue, action)
})
// 测试来自插件的请求头处理
t.Run("request from this plugin", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置来自插件的请求头
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{EXTEND_HEADER_KEY, "true"},
{"Content-Type", "application/json"},
})
// 应该返回ActionContinue
require.Equal(t, types.ActionContinue, action)
})
// 测试没有Authorization头的请求
t.Run("no authorization header", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置没有Authorization的请求头
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
{"Content-Length", "100"},
})
// 应该返回ActionContinue
require.Equal(t, types.ActionContinue, action)
})
// 测试配置错误的请求头处理
t.Run("config error", func(t *testing.T) {
host, status := test.NewTestHost(missingRequiredConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置请求头
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
// 应该返回ActionPause
require.Equal(t, types.ActionPause, action)
})
})
}
func TestOnHttpRequestBody(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
// 测试来自插件的请求(应该直接继续)
t.Run("request from this plugin", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置请求头包含EXTEND_HEADER_KEY来标记请求来自插件
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
{EXTEND_HEADER_KEY, "true"},
})
// 设置请求体
body := `{
"model": "gpt-3.5-turbo",
"messages": [
{"role": "user", "content": "Hello"}
]
}`
action := host.CallOnHttpRequestBody([]byte(body))
// 应该返回ActionContinue因为请求来自插件
require.Equal(t, types.ActionContinue, action)
})
// 测试配置错误的请求体处理
t.Run("config error", func(t *testing.T) {
host, status := test.NewTestHost(missingRequiredConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置请求头
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
// 设置请求体
body := `{
"model": "gpt-3.5-turbo",
"messages": [
{"role": "user", "content": "Hello"}
]
}`
action := host.CallOnHttpRequestBody([]byte(body))
// 应该返回ActionContinue因为配置有错误
require.Equal(t, types.ActionContinue, action)
})
// 测试正常请求体处理 - 成功响应
t.Run("normal request with successful response", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置请求头
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
// 设置请求体
body := `{
"model": "gpt-3.5-turbo",
"messages": [
{"role": "user", "content": "What is AI?"}
]
}`
action := host.CallOnHttpRequestBody([]byte(body))
// 应该返回ActionPause等待外部服务响应
require.Equal(t, types.ActionPause, action)
// 模拟外部服务返回成功响应
host.CallOnHttpCall([][2]string{
{":status", "200"},
{"content-type", "application/json"},
}, []byte(`{
"id": "chatcmpl-123",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "{\"definition\": \"AI is artificial intelligence\", \"examples\": [\"machine learning\", \"natural language processing\"]}"
}
}
]
}`))
response := host.GetLocalResponse()
require.NotNil(t, response)
require.Contains(t, string(response.Data), "definition")
require.Contains(t, string(response.Data), "examples")
// 完成HTTP请求
host.CompleteHttp()
})
// 测试正常请求体处理 - 需要重试的响应
t.Run("normal request with retry response", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置请求头
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
// 设置请求体
body := `{
"model": "gpt-3.5-turbo",
"messages": [
{"role": "user", "content": "What is AI?"}
]
}`
action := host.CallOnHttpRequestBody([]byte(body))
// 应该返回ActionPause等待外部服务响应
require.Equal(t, types.ActionPause, action)
// 模拟外部服务返回需要重试的响应content字段不是有效JSON
host.CallOnHttpCall([][2]string{
{":status", "200"},
{"content-type", "application/json"},
}, []byte(`{
"id": "chatcmpl-123",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "AI is artificial intelligence. It includes machine learning and natural language processing."
}
}
]
}`))
// 由于content不是有效JSON插件会进行重试
// 模拟重试请求的响应
host.CallOnHttpCall([][2]string{
{":status", "200"},
{"content-type", "application/json"},
}, []byte(`{
"id": "chatcmpl-456",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "{\"definition\": \"AI is artificial intelligence\", \"examples\": [\"machine learning\", \"natural language processing\"]}"
}
}
]
}`))
// 验证最终响应体是提取的JSON内容
response := host.GetLocalResponse()
require.NotNil(t, response)
require.Contains(t, string(response.Data), "definition")
require.Contains(t, string(response.Data), "examples")
// 完成HTTP请求
host.CompleteHttp()
})
// 测试外部服务返回无效响应体
t.Run("external service returns invalid response body", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置请求头
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
// 设置请求体
body := `{
"model": "gpt-3.5-turbo",
"messages": [
{"role": "user", "content": "What is AI?"}
]
}`
action := host.CallOnHttpRequestBody([]byte(body))
// 应该返回ActionPause等待外部服务响应
require.Equal(t, types.ActionPause, action)
// 模拟外部服务返回无效的响应体
host.CallOnHttpCall([][2]string{
{":status", "200"},
{"content-type", "application/json"},
}, []byte(`invalid json response`))
// 验证响应体包含错误信息
response := host.GetLocalResponse()
require.NotNil(t, response)
require.Contains(t, string(response.Data), "invalid json response")
// 完成HTTP请求
host.CompleteHttp()
})
// 测试外部服务返回缺少content字段的响应
t.Run("external service returns response without content field", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置请求头
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
// 设置请求体
body := `{
"model": "gpt-3.5-turbo",
"messages": [
{"role": "user", "content": "What is AI?"}
]
}`
action := host.CallOnHttpRequestBody([]byte(body))
// 应该返回ActionPause等待外部服务响应
require.Equal(t, types.ActionPause, action)
// 模拟外部服务返回缺少content字段的响应
host.CallOnHttpCall([][2]string{
{":status", "200"},
{"content-type", "application/json"},
}, []byte(`{
"id": "chatcmpl-123",
"choices": [
{
"index": 0,
"message": {
"role": "assistant"
}
}
]
}`))
// 验证响应体包含错误信息
response := host.GetLocalResponse()
require.NotNil(t, response)
require.Contains(t, string(response.Data), "response body does not contain the content")
// 完成HTTP请求
host.CompleteHttp()
})
// 测试使用自定义servicePath的请求
t.Run("request with custom service path", func(t *testing.T) {
host, status := test.NewTestHost(serviceUrlConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置请求头
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/custom/chat"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
// 设置请求体
body := `{
"model": "gpt-3.5-turbo",
"messages": [
{"role": "user", "content": "What is AI?"}
]
}`
action := host.CallOnHttpRequestBody([]byte(body))
// 应该返回ActionPause等待外部服务响应
require.Equal(t, types.ActionPause, action)
// 模拟外部服务返回成功响应
host.CallOnHttpCall([][2]string{
{":status", "200"},
{"content-type", "application/json"},
}, []byte(`{
"id": "chatcmpl-123",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "{\"answer\": \"AI is artificial intelligence\"}"
}
}
]
}`))
// 验证响应体是提取的JSON内容
response := host.GetLocalResponse()
require.NotNil(t, response)
require.Contains(t, string(response.Data), "answer")
// 完成HTTP请求
host.CompleteHttp()
})
// 测试达到最大重试次数的情况
t.Run("max retry count exceeded", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 设置请求头
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
// 设置请求体
body := `{
"model": "gpt-3.5-turbo",
"messages": [
{"role": "user", "content": "What is AI?"}
]
}`
action := host.CallOnHttpRequestBody([]byte(body))
// 应该返回ActionPause等待外部服务响应
require.Equal(t, types.ActionPause, action)
// 模拟多次重试每次都返回无效的content
for i := 0; i < 4; i++ { // 超过最大重试次数3次
host.CallOnHttpCall([][2]string{
{":status", "200"},
{"content-type", "application/json"},
}, []byte(`{
"id": "chatcmpl-123",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "AI is artificial intelligence"
}
}
]
}`))
}
// 验证最终响应体包含重试次数超限的错误信息
response := host.GetLocalResponse()
require.NotNil(t, response)
require.Contains(t, string(response.Data), "retry count exceeds max retry count")
// 完成HTTP请求
host.CompleteHttp()
})
})
}
func TestRejectStruct(t *testing.T) {
// 测试RejectStruct的GetBytes方法
t.Run("GetBytes", func(t *testing.T) {
reject := RejectStruct{
RejectCode: 1001,
RejectMsg: "Test error message",
}
bytes := reject.GetBytes()
require.NotNil(t, bytes)
// 验证JSON格式
var result RejectStruct
err := json.Unmarshal(bytes, &result)
require.NoError(t, err)
require.Equal(t, uint32(1001), result.RejectCode)
require.Equal(t, "Test error message", result.RejectMsg)
})
// 测试RejectStruct的GetShortMsg方法
t.Run("GetShortMsg", func(t *testing.T) {
reject := RejectStruct{
RejectCode: 1001,
RejectMsg: "Json Schema is not valid: invalid format",
}
shortMsg := reject.GetShortMsg()
require.Equal(t, "ai-json-resp.Json Schema is not valid", shortMsg)
})
// 测试RejectStruct的GetShortMsg方法 - 没有冒号的情况
t.Run("GetShortMsg no colon", func(t *testing.T) {
reject := RejectStruct{
RejectCode: 1001,
RejectMsg: "Simple error message",
}
shortMsg := reject.GetShortMsg()
require.Equal(t, "ai-json-resp.Simple error message", shortMsg)
})
}
func TestValidateBody(t *testing.T) {
// 创建测试配置
config := &PluginConfig{
contentPath: "choices.0.message.content",
jsonSchema: nil, // 明确设置为nil禁用JSON Schema验证
enableJsonSchemaValidation: false, // 禁用JSON Schema验证
}
// 测试有效的响应体
t.Run("valid response body", func(t *testing.T) {
validBody := []byte(`{
"id": "chatcmpl-123",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello, how can I help you?"
}
}
]
}`)
err := config.ValidateBody(validBody)
require.NoError(t, err)
})
// 测试无效的JSON响应体
t.Run("invalid JSON response body", func(t *testing.T) {
invalidBody := []byte(`invalid json content`)
err := config.ValidateBody(invalidBody)
require.Error(t, err)
require.Equal(t, uint32(SERVICE_UNAVAILABLE_CODE), config.rejectStruct.RejectCode)
require.Contains(t, config.rejectStruct.RejectMsg, "service unavailable")
})
// 测试缺少content字段的响应体
t.Run("missing content field", func(t *testing.T) {
missingContentBody := []byte(`{
"id": "chatcmpl-123",
"choices": [
{
"index": 0,
"message": {
"role": "assistant"
}
}
]
}`)
err := config.ValidateBody(missingContentBody)
require.Error(t, err)
require.Equal(t, uint32(SERVICE_UNAVAILABLE_CODE), config.rejectStruct.RejectCode)
require.Contains(t, config.rejectStruct.RejectMsg, "response body does not contain the content")
})
// 测试空的响应体
t.Run("empty response body", func(t *testing.T) {
emptyBody := []byte{}
err := config.ValidateBody(emptyBody)
require.Error(t, err)
require.Equal(t, uint32(SERVICE_UNAVAILABLE_CODE), config.rejectStruct.RejectCode)
})
}
func TestExtractJson(t *testing.T) {
// 创建测试配置
config := &PluginConfig{
jsonSchema: nil, // 明确设置为nil禁用JSON Schema验证
enableJsonSchemaValidation: false, // 禁用JSON Schema验证
}
// 测试提取有效的JSON
t.Run("extract valid JSON", func(t *testing.T) {
content := `Here is the response: {"name": "John", "age": 30} and some other text`
jsonStr, err := config.ExtractJson(content)
require.NoError(t, err)
require.Equal(t, `{"name": "John", "age": 30}`, jsonStr)
})
// 测试提取嵌套JSON
t.Run("extract nested JSON", func(t *testing.T) {
content := `Response: {"user": {"name": "John", "profile": {"age": 30, "city": "NYC"}}}`
jsonStr, err := config.ExtractJson(content)
require.NoError(t, err)
require.Equal(t, `{"user": {"name": "John", "profile": {"age": 30, "city": "NYC"}}}`, jsonStr)
})
// 测试没有JSON的内容
t.Run("no JSON in content", func(t *testing.T) {
content := `This is just plain text without any JSON content`
_, err := config.ExtractJson(content)
require.Error(t, err)
require.Contains(t, err.Error(), "cannot find json in the response body")
})
// 测试只有开始括号的内容
t.Run("only opening brace", func(t *testing.T) {
content := `Here is the start: { but no closing brace`
_, err := config.ExtractJson(content)
require.Error(t, err)
require.Contains(t, err.Error(), "cannot find json in the response body")
})
// 测试只有结束括号的内容
t.Run("only closing brace", func(t *testing.T) {
content := `Here is the end: } but no opening brace`
_, err := config.ExtractJson(content)
require.Error(t, err)
require.Contains(t, err.Error(), "cannot find json in the response body")
})
// 测试无效的JSON格式
t.Run("invalid JSON format", func(t *testing.T) {
content := `Here is invalid JSON: {"name": "John", "age": 30,}`
_, err := config.ExtractJson(content)
require.Error(t, err)
// ExtractJson会提取到{"name": "John", "age": 30,}但json.Unmarshal会失败
// 因为JSON格式无效末尾有多余的逗号
require.Contains(t, err.Error(), "invalid character '}' looking for beginning of object key string")
})
// 测试多个JSON对象应该提取第一个完整的
t.Run("multiple JSON objects", func(t *testing.T) {
content := `First: {"name": "John"} Second: {"age": 30}`
_, err := config.ExtractJson(content)
require.Error(t, err)
// ExtractJson会提取到{"name": "John"} Second: {"age": 30}
// 这不是有效的JSON因为"Second: {"age": 30}"不是有效的JSON语法
require.Contains(t, err.Error(), "invalid character 'S' after top-level value")
})
}