Files
oci-portal/internal/oci/errors_test.go
T

271 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
})
}
}