feat(storage): 统一二进制对象存储与公开错误
新增 Aliyun OSS 与 S3 协议、通道内重试和按优先级跨通道切换,保留 server-main 兼容与环境 OSS 内存通道。 将请求及结果中的 Base64、Data URI、Buffer、multipart 和内联二进制统一对象化,生产路径不再写入本机静态目录,历史本地资源仅保留只读兼容。 引入 PublicErrorV1 并统一 API、异步查询、兼容协议和失败回调的安全错误输出,同时补充迁移、管理端、指标、OpenAPI 与本地模拟验收。 验证:go test ./... -count=1;go vet ./...;pnpm lint;pnpm test;pnpm build;pnpm openapi;tests/ci/migrations-test.sh。
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/acceptanceemulator"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
func TestLocalAcceptanceObjectStorageFailoverMatrix(t *testing.T) {
|
||||
emulator := httptest.NewServer(acceptanceemulator.New(acceptanceemulator.Config{}).Handler())
|
||||
defer emulator.Close()
|
||||
service := &Service{}
|
||||
payload := FileUploadPayload{
|
||||
Bytes: []byte("acceptance-object"), ContentType: "image/png", Scene: store.FileStorageSceneRequestAsset,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
channels []store.FileStorageChannel
|
||||
winner string
|
||||
wantErrCode string
|
||||
}{
|
||||
{
|
||||
name: "oss_auth_to_s3",
|
||||
channels: []store.FileStorageChannel{
|
||||
acceptanceStorageChannel("aliyun_oss", emulator.URL+"/storage/oss-auth/bucket", "oss-auth", false),
|
||||
acceptanceStorageChannel("s3", emulator.URL+"/storage/s3", "s3-ok", false),
|
||||
},
|
||||
winner: "s3-ok",
|
||||
},
|
||||
{
|
||||
name: "s3_auth_to_oss",
|
||||
channels: []store.FileStorageChannel{
|
||||
acceptanceStorageChannel("s3", emulator.URL+"/storage/s3-auth", "s3-auth", false),
|
||||
acceptanceStorageChannel("aliyun_oss", emulator.URL+"/storage/oss/bucket", "oss-ok", false),
|
||||
},
|
||||
winner: "oss-ok",
|
||||
},
|
||||
{
|
||||
name: "same_channel_transient_retry",
|
||||
channels: []store.FileStorageChannel{
|
||||
acceptanceStorageChannel("s3", emulator.URL+"/storage/s3-transient", "s3-transient", true),
|
||||
acceptanceStorageChannel("aliyun_oss", emulator.URL+"/storage/oss/bucket", "oss-unused", false),
|
||||
},
|
||||
winner: "s3-transient",
|
||||
},
|
||||
{
|
||||
name: "all_channels_failed",
|
||||
channels: []store.FileStorageChannel{
|
||||
acceptanceStorageChannel("aliyun_oss", emulator.URL+"/storage/oss-fail/bucket", "oss-fail", false),
|
||||
acceptanceStorageChannel("s3", emulator.URL+"/storage/s3-fail", "s3-fail", false),
|
||||
},
|
||||
wantErrCode: "storage_write_failed",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := service.uploadFileWithFailover(t.Context(), payload, test.channels)
|
||||
if test.wantErrCode != "" {
|
||||
if clients.ErrorCode(err) != test.wantErrCode {
|
||||
t.Fatalf("error=%v code=%s", err, clients.ErrorCode(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
channel, _ := result["storageChannel"].(map[string]any)
|
||||
if got := stringFromAny(channel["channelKey"]); got != test.winner {
|
||||
t.Fatalf("winner=%q, want %q", got, test.winner)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
response, err := http.Get(emulator.URL + "/report")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
var report acceptanceemulator.Report
|
||||
if err := json.NewDecoder(response.Body).Decode(&report); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if report.StoragePuts != 3 || report.StorageFailures != 5 {
|
||||
t.Fatalf("unexpected storage acceptance report: %+v", report)
|
||||
}
|
||||
}
|
||||
|
||||
func acceptanceStorageChannel(provider string, endpoint string, key string, retry bool) store.FileStorageChannel {
|
||||
channel := testObjectStorageChannel(provider, endpoint, key)
|
||||
channel.Config["publicBaseUrl"] = ""
|
||||
channel.RetryPolicy = map[string]any{"enabled": retry, "maxRetries": 0}
|
||||
if retry {
|
||||
channel.RetryPolicy = map[string]any{"enabled": true, "maxRetries": 1, "backoffSeconds": []any{0.001}}
|
||||
}
|
||||
return channel
|
||||
}
|
||||
Reference in New Issue
Block a user