271 lines
9.1 KiB
Go
271 lines
9.1 KiB
Go
package oci
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"net/url"
|
||
"testing"
|
||
)
|
||
|
||
// fakeServiceError 模拟 oci-go-sdk 的 common.ServiceError,
|
||
// Error() 按 SDK 真实格式输出长文以验证截断。
|
||
type fakeServiceError struct {
|
||
status int
|
||
code string
|
||
message string
|
||
}
|
||
|
||
func (e fakeServiceError) Error() string {
|
||
return fmt.Sprintf(
|
||
"Error returned by Compute Service. Http Status Code: %d. Error Code: %s. Message: %s. Troubleshooting Tips: See https://docs.oracle.com/ ...",
|
||
e.status, e.code, e.message)
|
||
}
|
||
func (e fakeServiceError) GetHTTPStatusCode() int { return e.status }
|
||
func (e fakeServiceError) GetMessage() string { return e.message }
|
||
func (e fakeServiceError) GetCode() string { return e.code }
|
||
func (e fakeServiceError) GetOpcRequestID() string { return "req-1" }
|
||
|
||
func TestCompactError(t *testing.T) {
|
||
capacity := fakeServiceError{status: 500, code: "InternalError", message: "Out of host capacity."}
|
||
tests := []struct {
|
||
name string
|
||
err error
|
||
want string
|
||
}{
|
||
{
|
||
name: "非 OCI 错误原样返回",
|
||
err: errors.New("parse payload: unexpected end of JSON input"),
|
||
want: "parse payload: unexpected end of JSON input",
|
||
},
|
||
{
|
||
name: "带操作前缀的 OCI 错误保留前缀并剥掉长文",
|
||
err: fmt.Errorf("launch instance: %w", capacity),
|
||
want: "launch instance: Out of host capacity.",
|
||
},
|
||
{
|
||
name: "多层包装保留完整前缀链",
|
||
err: fmt.Errorf("snatch-1: %w", fmt.Errorf("launch instance: %w", capacity)),
|
||
want: "snatch-1: launch instance: Out of host capacity.",
|
||
},
|
||
{
|
||
name: "无前缀时只输出服务端消息",
|
||
err: error(capacity),
|
||
want: "Out of host capacity.",
|
||
},
|
||
{
|
||
name: "消息中的长 OCID 压缩为类型加尾位",
|
||
err: fmt.Errorf("update instance: %w", fakeServiceError{
|
||
status: 500, code: "InternalError",
|
||
message: "instance ocid1.instance.oc1.phx.anyhqljtp537sbqcr2f5ed2jwkglwyd3g6dfbmgtwdp4xdarxum5q7tdcyla: Out of host capacity.",
|
||
}),
|
||
want: "update instance: instance ocid1.instance…tdcyla: Out of host capacity.",
|
||
},
|
||
{
|
||
name: "非 OCI 错误里的 OCID 同样压缩",
|
||
err: errors.New("get instance ocid1.instance.oc1..aaaaaaaabbbbbbbbccccccccdddddddd failed"),
|
||
want: "get instance ocid1.instance…dddddd failed",
|
||
},
|
||
}
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
if got := CompactError(tt.err); got != tt.want {
|
||
t.Errorf("CompactError() = %q, want %q", got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestErrorHint(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
err error
|
||
want string
|
||
}{
|
||
{
|
||
name: "非 OCI 错误无提示",
|
||
err: errors.New("db locked"),
|
||
want: "",
|
||
},
|
||
{
|
||
name: "已知错误码给出中文提示",
|
||
err: fmt.Errorf("delete vcn: %w", fakeServiceError{409, "IncorrectState", "vcn in use"}),
|
||
want: ociErrorHints["IncorrectState"],
|
||
},
|
||
{
|
||
name: "InternalError 但消息为容量不足时归到容量提示",
|
||
err: fmt.Errorf("launch: %w", fakeServiceError{500, "InternalError", "Out of host capacity."}),
|
||
want: ociErrorHints["OutOfHostCapacity"],
|
||
},
|
||
{
|
||
name: "NotAuthenticated 提示密钥无效或传播中",
|
||
err: fmt.Errorf("get instance: %w", fakeServiceError{401, "NotAuthenticated", "The required information ..."}),
|
||
want: ociErrorHints["NotAuthenticated"],
|
||
},
|
||
{
|
||
name: "未知错误码返回空串",
|
||
err: fmt.Errorf("x: %w", fakeServiceError{400, "SomethingNew", "boom"}),
|
||
want: "",
|
||
},
|
||
}
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
if got := ErrorHint(tt.err); got != tt.want {
|
||
t.Errorf("ErrorHint() = %q, want %q", got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestIsOnDemandUnsupported(t *testing.T) {
|
||
ft := fakeServiceError{status: 400, code: "InvalidParameter",
|
||
message: "Not allowed to call finetune base model ocid1.generativeaimodel.oc1.eu-frankfurt-1.tpel5q, use Endpoint: false"}
|
||
tests := []struct {
|
||
name string
|
||
err error
|
||
want bool
|
||
}{
|
||
{"微调基座 400 命中(含包装链)", fmt.Errorf("genai chat: %w", ft), true},
|
||
{"同消息但非 400 不命中", fakeServiceError{status: 500, code: "InternalError", message: ft.message}, false},
|
||
{"普通 400 不命中", fakeServiceError{status: 400, code: "InvalidParameter", message: "bad request"}, false},
|
||
{"非服务端错误不命中", errors.New("finetune base model"), false},
|
||
}
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
if got := IsOnDemandUnsupported(tt.err); got != tt.want {
|
||
t.Errorf("IsOnDemandUnsupported() = %v, want %v", got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestIsModelUnavailable(t *testing.T) {
|
||
entity404 := fakeServiceError{status: 404, code: "NotFound",
|
||
message: "Entity with key ocid1.generativeaimodel.oc1.eu-frankfurt-1.2flsfq not found"}
|
||
auth404 := fakeServiceError{status: 404, code: "NotAuthorizedOrNotFound",
|
||
message: "Authorization failed or requested resource not found."}
|
||
ft400 := fakeServiceError{status: 400, code: "InvalidParameter",
|
||
message: "Not allowed to call finetune base model ocid1.generativeaimodel.oc1..x, use Endpoint: false"}
|
||
tests := []struct {
|
||
name string
|
||
err error
|
||
want bool
|
||
}{
|
||
{"实体不存在 404 命中(含包装链)", fmt.Errorf("genai chat: %w", entity404), true},
|
||
{"鉴权类 404 不命中(仍属租户级)", auth404, false},
|
||
{"微调基座 400 命中", ft400, true},
|
||
{"其他 404 消息不命中", fakeServiceError{status: 404, code: "NotFound", message: "route not found"}, false},
|
||
{"非服务端错误不命中", errors.New("entity with key x not found"), false},
|
||
}
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
if got := IsModelUnavailable(tt.err); got != tt.want {
|
||
t.Errorf("IsModelUnavailable() = %v, want %v", got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// fakeTimeoutErr 模拟实现 net.Error 的超时错误(如响应体读取超时)。
|
||
type fakeTimeoutErr struct{}
|
||
|
||
func (fakeTimeoutErr) Error() string { return "read tcp 10.0.0.1:443: i/o timeout" }
|
||
func (fakeTimeoutErr) Timeout() bool { return true }
|
||
func (fakeTimeoutErr) Temporary() bool { return false }
|
||
|
||
type upstreamNetworkErrorCase struct {
|
||
name string
|
||
err error
|
||
want string
|
||
wantOK bool
|
||
}
|
||
|
||
var upstreamNetworkErrorCases = []upstreamNetworkErrorCase{
|
||
{
|
||
name: "OCI 服务错误不归网络",
|
||
err: fmt.Errorf("get instance: %w", fakeServiceError{status: 401, code: "NotAuthenticated", message: "x"}),
|
||
wantOK: false,
|
||
},
|
||
{
|
||
name: "普通业务错误不归网络",
|
||
err: errors.New("parse payload: bad json"),
|
||
wantOK: false,
|
||
},
|
||
{
|
||
name: "代理 CONNECT 失败带中文分类且 URL 缩为主机",
|
||
err: fmt.Errorf("list instances: %w", &url.Error{
|
||
Op: "Get",
|
||
URL: "https://iaas.uk-london-1.oraclecloud.com/20160918/instances?limit=100",
|
||
Err: errors.New("proxyconnect tcp: dial tcp 1.2.3.4:8080: connect: connection refused"),
|
||
}),
|
||
want: "代理连接失败: Get iaas.uk-london-1.oraclecloud.com: proxyconnect tcp: dial tcp 1.2.3.4:8080: connect: connection refused",
|
||
wantOK: true,
|
||
},
|
||
{
|
||
name: "SOCKS 握手失败归代理类",
|
||
err: &url.Error{
|
||
Op: "Post",
|
||
URL: "https://identity.us-sanjose-1.oci.oraclecloud.com/20160918/users",
|
||
Err: errors.New("socks connect tcp 5.6.7.8:1080->identity: dial refused"),
|
||
},
|
||
want: "代理连接失败: Post identity.us-sanjose-1.oci.oraclecloud.com: socks connect tcp 5.6.7.8:1080->identity: dial refused",
|
||
wantOK: true,
|
||
},
|
||
{
|
||
name: "URL 内嵌 userinfo 不回显",
|
||
err: &url.Error{
|
||
Op: "Get",
|
||
URL: "https://user:pass@example.com/path",
|
||
Err: errors.New("EOF"),
|
||
},
|
||
want: "上游连接失败: Get example.com: EOF",
|
||
wantOK: true,
|
||
},
|
||
{
|
||
name: "嵌套 URL 的代理 userinfo 不回显",
|
||
err: &url.Error{
|
||
Op: "Get",
|
||
URL: "https://identity.us-ashburn-1.oraclecloud.com/20160918/tenancies/x",
|
||
Err: fmt.Errorf("proxyconnect tcp: %w", &url.Error{
|
||
Op: "Dial", URL: "http://proxy-user:proxy-pass@proxy.example.com:8080",
|
||
Err: errors.New("connect: connection refused"),
|
||
}),
|
||
},
|
||
want: "代理连接失败: Get identity.us-ashburn-1.oraclecloud.com: proxyconnect tcp: Dial proxy.example.com:8080: connect: connection refused",
|
||
wantOK: true,
|
||
},
|
||
{
|
||
name: "普通内层错误中的存量代理 userinfo 不回显",
|
||
err: &url.Error{
|
||
Op: "Get",
|
||
URL: "https://identity.us-phoenix-1.oraclecloud.com/20160918/tenancies/x",
|
||
Err: errors.New("proxyconnect tcp: dial tcp alice:secret@proxy.example.com:8080: connect: connection refused"),
|
||
},
|
||
want: "代理连接失败: Get identity.us-phoenix-1.oraclecloud.com: proxyconnect tcp: dial tcp proxy.example.com:8080: connect: connection refused",
|
||
wantOK: true,
|
||
},
|
||
{
|
||
name: "context 超时归网络",
|
||
err: fmt.Errorf("summarize costs: %w", context.DeadlineExceeded),
|
||
want: "上游连接失败: 请求超时",
|
||
wantOK: true,
|
||
},
|
||
{
|
||
name: "net.Error 超时归网络",
|
||
err: fmt.Errorf("read body: %w", fakeTimeoutErr{}),
|
||
want: "上游连接失败: 连接超时",
|
||
wantOK: true,
|
||
},
|
||
}
|
||
|
||
func TestUpstreamNetworkError(t *testing.T) {
|
||
for _, tt := range upstreamNetworkErrorCases {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
got, ok := UpstreamNetworkError(tt.err)
|
||
if ok != tt.wantOK || got != tt.want {
|
||
t.Errorf("UpstreamNetworkError() = (%q, %v), want (%q, %v)", got, ok, tt.want, tt.wantOK)
|
||
}
|
||
})
|
||
}
|
||
}
|