542 lines
18 KiB
Go
542 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"oci-portal/internal/model"
|
|
"oci-portal/internal/oci"
|
|
)
|
|
|
|
type osStubClient struct {
|
|
*fakeClient
|
|
|
|
buckets []oci.Bucket
|
|
createdPAR oci.CreatePARInput
|
|
renamed [2]string
|
|
|
|
// 对象内容中转 stub 状态
|
|
objectData []byte
|
|
objectEtag string
|
|
putContentType string
|
|
putIfMatch string
|
|
|
|
// 后台清空删除桶的编排状态,goroutine 并发访问需加锁
|
|
mu sync.Mutex
|
|
versions []oci.ObjectVersion
|
|
pars []oci.PAR
|
|
bucketDeleted bool
|
|
multipartAborted bool
|
|
listCalls int
|
|
}
|
|
|
|
func (f *osStubClient) AbortAllMultipartUploads(ctx context.Context, cred oci.Credentials, region, bucket string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.multipartAborted = true
|
|
return nil
|
|
}
|
|
|
|
func (f *osStubClient) ListBuckets(ctx context.Context, cred oci.Credentials, region, compartmentID string) ([]oci.Bucket, error) {
|
|
return f.buckets, nil
|
|
}
|
|
|
|
func (f *osStubClient) CreateBucket(ctx context.Context, cred oci.Credentials, region string, in oci.CreateBucketInput) (oci.Bucket, error) {
|
|
b := oci.Bucket{Name: in.Name, Namespace: "ns", VersioningOn: in.VersioningOn}
|
|
f.buckets = append(f.buckets, b)
|
|
return b, nil
|
|
}
|
|
|
|
func (f *osStubClient) RenameObject(ctx context.Context, cred oci.Credentials, region, bucket, src, dst string) error {
|
|
f.renamed = [2]string{src, dst}
|
|
return nil
|
|
}
|
|
|
|
func (f *osStubClient) DeleteBucket(ctx context.Context, cred oci.Credentials, region, name string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
// 模拟 OCI 真实行为:残留对象版本或活跃 PAR 都会拒绝删桶
|
|
if len(f.versions) > 0 || len(f.pars) > 0 {
|
|
return fmt.Errorf("delete bucket %s: %w", name, oci.ErrBucketNotEmpty)
|
|
}
|
|
f.bucketDeleted = true
|
|
return nil
|
|
}
|
|
|
|
func (f *osStubClient) ListObjectVersions(ctx context.Context, cred oci.Credentials, region, bucket, page string) ([]oci.ObjectVersion, string, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.listCalls++
|
|
return append([]oci.ObjectVersion(nil), f.versions...), "", nil
|
|
}
|
|
|
|
func (f *osStubClient) DeleteObjectVersion(ctx context.Context, cred oci.Credentials, region, bucket, object, versionID string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
kept := f.versions[:0]
|
|
for _, v := range f.versions {
|
|
if !(v.Name == object && v.VersionID == versionID) {
|
|
kept = append(kept, v)
|
|
}
|
|
}
|
|
f.versions = kept
|
|
return nil
|
|
}
|
|
|
|
func (f *osStubClient) ListPARs(ctx context.Context, cred oci.Credentials, region, bucket string) ([]oci.PAR, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return append([]oci.PAR(nil), f.pars...), nil
|
|
}
|
|
|
|
// ListPARsPage 以数组下标模拟游标:page 为起始下标,next 为下一段起始下标。
|
|
func (f *osStubClient) ListPARsPage(ctx context.Context, cred oci.Credentials, region, bucket, page string, limit int) ([]oci.PAR, string, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
start, _ := strconv.Atoi(page)
|
|
if start >= len(f.pars) || limit <= 0 {
|
|
return nil, "", nil
|
|
}
|
|
end := start + limit
|
|
next := ""
|
|
if end < len(f.pars) {
|
|
next = strconv.Itoa(end)
|
|
} else {
|
|
end = len(f.pars)
|
|
}
|
|
return append([]oci.PAR(nil), f.pars[start:end]...), next, nil
|
|
}
|
|
|
|
func (f *osStubClient) DeletePAR(ctx context.Context, cred oci.Credentials, region, bucket, parID string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
kept := f.pars[:0]
|
|
for _, p := range f.pars {
|
|
if p.ID != parID {
|
|
kept = append(kept, p)
|
|
}
|
|
}
|
|
f.pars = kept
|
|
return nil
|
|
}
|
|
|
|
func (f *osStubClient) purgeState() (deleted bool, versions, pars int) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.bucketDeleted, len(f.versions), len(f.pars)
|
|
}
|
|
|
|
func (f *osStubClient) GetObject(ctx context.Context, cred oci.Credentials, region, bucket, object string, maxBytes int64) (oci.ObjectContent, error) {
|
|
if int64(len(f.objectData)) > maxBytes {
|
|
return oci.ObjectContent{}, fmt.Errorf("get object %s: %w", object, oci.ErrObjectTooLarge)
|
|
}
|
|
return oci.ObjectContent{Data: f.objectData, ContentType: "text/plain", Etag: f.objectEtag}, nil
|
|
}
|
|
|
|
func (f *osStubClient) PutObject(ctx context.Context, cred oci.Credentials, region, bucket, object string, data []byte, contentType, ifMatch string) (string, error) {
|
|
f.objectData = data
|
|
f.putContentType = contentType
|
|
f.putIfMatch = ifMatch
|
|
return "etag-new", nil
|
|
}
|
|
|
|
func (f *osStubClient) CreatePAR(ctx context.Context, cred oci.Credentials, region, bucket string, in oci.CreatePARInput) (oci.PAR, error) {
|
|
f.createdPAR = in
|
|
return oci.PAR{ID: "par-1", Name: in.Name, AccessType: in.AccessType, FullURL: "https://os.example/p/x/n/ns/b/o"}, nil
|
|
}
|
|
|
|
func TestObjectStorageValidation(t *testing.T) {
|
|
client := &osStubClient{fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}}
|
|
svc := newTestService(t, client)
|
|
cfg := importAliveConfig(t, svc)
|
|
ctx := context.Background()
|
|
|
|
tests := []struct {
|
|
name string
|
|
run func() error
|
|
wantErr string
|
|
}{
|
|
{name: "桶名非法字符", run: func() error {
|
|
_, err := svc.CreateBucket(ctx, cfg.ID, "r", oci.CreateBucketInput{Name: "有中文"})
|
|
return err
|
|
}, wantErr: "invalid character"},
|
|
{name: "桶名空", run: func() error {
|
|
_, err := svc.CreateBucket(ctx, cfg.ID, "r", oci.CreateBucketInput{Name: ""})
|
|
return err
|
|
}, wantErr: "length"},
|
|
{name: "合法桶名", run: func() error {
|
|
_, err := svc.CreateBucket(ctx, cfg.ID, "r", oci.CreateBucketInput{Name: "my-bucket_01"})
|
|
return err
|
|
}},
|
|
{name: "重命名缺参", run: func() error {
|
|
return svc.RenameObject(ctx, cfg.ID, "r", "b", "a.txt", "a.txt")
|
|
}, wantErr: "distinct"},
|
|
{name: "PAR 类型不支持", run: func() error {
|
|
_, err := svc.CreatePAR(ctx, cfg.ID, "r", "b", oci.CreatePARInput{AccessType: "Whatever", ExpiresHours: 2})
|
|
return err
|
|
}, wantErr: "unsupported access type"},
|
|
{name: "PAR 时长过短", run: func() error {
|
|
_, err := svc.CreatePAR(ctx, cfg.ID, "r", "b", oci.CreatePARInput{AccessType: "ObjectRead", ExpiresHours: 0})
|
|
return err
|
|
}, wantErr: "1-876000"},
|
|
// 旧 30 天上限已放开:10 年应放行
|
|
{name: "PAR 超长有效期放行", run: func() error {
|
|
_, err := svc.CreatePAR(ctx, cfg.ID, "r", "b", oci.CreatePARInput{AccessType: "ObjectRead", ExpiresHours: 24 * 3650})
|
|
return err
|
|
}},
|
|
// 防 time.Duration 溢出的安全上限(100 年)仍要拦截
|
|
{name: "PAR 超安全上限拒绝", run: func() error {
|
|
_, err := svc.CreatePAR(ctx, cfg.ID, "r", "b", oci.CreatePARInput{AccessType: "ObjectRead", ExpiresHours: maxParHours + 1})
|
|
return err
|
|
}, wantErr: "1-876000"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.run()
|
|
if tt.wantErr == "" {
|
|
if err != nil {
|
|
t.Fatalf("err = %v, want nil", err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Fatalf("err = %v, want contains %q", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeleteBucketEmptySync(t *testing.T) {
|
|
client := &osStubClient{fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}}
|
|
svc := newTestService(t, client)
|
|
cfg := importAliveConfig(t, svc)
|
|
|
|
queued, err := svc.DeleteBucket(context.Background(), cfg.ID, "r", "b")
|
|
if err != nil || queued {
|
|
t.Fatalf("DeleteBucket 空桶 = queued %v, %v, want 同步删除", queued, err)
|
|
}
|
|
if deleted, _, _ := client.purgeState(); !deleted {
|
|
t.Error("空桶应同步删除")
|
|
}
|
|
}
|
|
|
|
func TestDeleteBucketPurgeQueued(t *testing.T) {
|
|
client := &osStubClient{
|
|
fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}},
|
|
versions: []oci.ObjectVersion{
|
|
{Name: "a.txt", VersionID: "v1"},
|
|
{Name: "a.txt", VersionID: ""},
|
|
{Name: "dir/b.bin", VersionID: "v9"},
|
|
},
|
|
pars: []oci.PAR{{ID: "p1"}, {ID: "p2"}},
|
|
}
|
|
svc := newTestService(t, client)
|
|
cfg := importAliveConfig(t, svc)
|
|
|
|
queued, err := svc.DeleteBucket(context.Background(), cfg.ID, "r", "b")
|
|
if err != nil || !queued {
|
|
t.Fatalf("DeleteBucket 非空桶 = queued %v, %v, want queued", queued, err)
|
|
}
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if deleted, nv, np := client.purgeState(); deleted && nv == 0 && np == 0 {
|
|
return
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
deleted, nv, np := client.purgeState()
|
|
t.Fatalf("purge 未完成: deleted=%v versions=%d pars=%d", deleted, nv, np)
|
|
}
|
|
|
|
// TestDeleteBucketPAROnlyQueued 覆盖真实环境踩到的场景:桶内无对象,
|
|
// 仅剩活跃 PAR 时 OCI 也拒绝删桶,应同样走后台清空后重删。
|
|
func TestDeleteBucketPAROnlyQueued(t *testing.T) {
|
|
client := &osStubClient{
|
|
fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}},
|
|
pars: []oci.PAR{{ID: "p1"}, {ID: "p2"}, {ID: "p3"}},
|
|
}
|
|
svc := newTestService(t, client)
|
|
cfg := importAliveConfig(t, svc)
|
|
|
|
queued, err := svc.DeleteBucket(context.Background(), cfg.ID, "r", "b")
|
|
if err != nil || !queued {
|
|
t.Fatalf("DeleteBucket 仅 PAR 桶 = queued %v, %v, want queued", queued, err)
|
|
}
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if deleted, _, np := client.purgeState(); deleted && np == 0 {
|
|
return
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
deleted, _, np := client.purgeState()
|
|
t.Fatalf("purge 未完成: deleted=%v pars=%d", deleted, np)
|
|
}
|
|
|
|
func TestDeleteAllPARsConcurrent(t *testing.T) {
|
|
pars := make([]oci.PAR, 40)
|
|
for i := range pars {
|
|
pars[i] = oci.PAR{ID: fmt.Sprintf("p-%d", i), Name: fmt.Sprintf("par-%d", i)}
|
|
}
|
|
client := &osStubClient{fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}, pars: pars}
|
|
svc := newTestService(t, client)
|
|
cfg := importAliveConfig(t, svc)
|
|
|
|
n, err := svc.DeleteAllPARs(context.Background(), cfg.ID, "r", "b")
|
|
if err != nil || n != 40 {
|
|
t.Fatalf("DeleteAllPARs = %d, %v, want 40 deleted", n, err)
|
|
}
|
|
if _, _, np := client.purgeState(); np != 0 {
|
|
t.Errorf("remaining pars = %d, want 0", np)
|
|
}
|
|
}
|
|
|
|
func TestPARsPage(t *testing.T) {
|
|
pars := make([]oci.PAR, 5)
|
|
for i := range pars {
|
|
pars[i] = oci.PAR{ID: fmt.Sprintf("p-%d", i)}
|
|
}
|
|
client := &osStubClient{fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}, pars: pars}
|
|
svc := newTestService(t, client)
|
|
cfg := importAliveConfig(t, svc)
|
|
|
|
tests := []struct {
|
|
name string
|
|
page string
|
|
limit int
|
|
wantIDs int
|
|
wantNext string
|
|
}{
|
|
{name: "首页带游标", page: "", limit: 2, wantIDs: 2, wantNext: "2"},
|
|
{name: "中间页", page: "2", limit: 2, wantIDs: 2, wantNext: "4"},
|
|
{name: "末页无游标", page: "4", limit: 2, wantIDs: 1, wantNext: ""},
|
|
{name: "limit 0 归一为 100", page: "", limit: 0, wantIDs: 5, wantNext: ""},
|
|
{name: "limit 越界归一为 100", page: "", limit: 5000, wantIDs: 5, wantNext: ""},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
items, next, err := svc.PARsPage(context.Background(), cfg.ID, "r", "b", tt.page, tt.limit)
|
|
if err != nil {
|
|
t.Fatalf("PARsPage: %v", err)
|
|
}
|
|
if len(items) != tt.wantIDs || next != tt.wantNext {
|
|
t.Fatalf("PARsPage = %d 条, next=%q; want %d 条, next=%q", len(items), next, tt.wantIDs, tt.wantNext)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestForEachConcurrently(t *testing.T) {
|
|
items := make([]int, 20)
|
|
for i := range items {
|
|
items[i] = i
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
items []int
|
|
failOn int // 值为 -1 表示不注入失败
|
|
wantErr bool
|
|
wantDone int
|
|
}{
|
|
{name: "全部成功", items: items, failOn: -1, wantDone: 20},
|
|
{name: "个别失败不中断其余", items: items, failOn: 3, wantErr: true, wantDone: 19},
|
|
{name: "空列表", items: nil, failOn: -1},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var (
|
|
mu sync.Mutex
|
|
done int
|
|
)
|
|
err := forEachConcurrently(4, tt.items, func(i int) error {
|
|
if i == tt.failOn {
|
|
return errors.New("boom")
|
|
}
|
|
mu.Lock()
|
|
done++
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
if (err != nil) != tt.wantErr || done != tt.wantDone {
|
|
t.Fatalf("forEachConcurrently err=%v done=%d, want err=%v done=%d", err, done, tt.wantErr, tt.wantDone)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestObjectContentRoundTrip(t *testing.T) {
|
|
client := &osStubClient{
|
|
fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}},
|
|
objectData: []byte("hello"), objectEtag: "e1",
|
|
}
|
|
svc := newTestService(t, client)
|
|
cfg := importAliveConfig(t, svc)
|
|
ctx := context.Background()
|
|
|
|
got, err := svc.ObjectContent(ctx, cfg.ID, "r", "b", "a.txt")
|
|
if err != nil || string(got.Data) != "hello" || got.Etag != "e1" {
|
|
t.Fatalf("ObjectContent = %q etag=%q, %v; want hello/e1", got.Data, got.Etag, err)
|
|
}
|
|
etag, err := svc.PutObjectContent(ctx, cfg.ID, "r", "b", "a.txt", []byte("world"), "text/plain", "e1")
|
|
if err != nil || etag != "etag-new" {
|
|
t.Fatalf("PutObjectContent = %q, %v; want etag-new", etag, err)
|
|
}
|
|
if client.putIfMatch != "e1" || client.putContentType != "text/plain" {
|
|
t.Errorf("ifMatch=%q contentType=%q, want e1/text-plain 透传", client.putIfMatch, client.putContentType)
|
|
}
|
|
}
|
|
|
|
func TestObjectContentLimits(t *testing.T) {
|
|
client := &osStubClient{fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}}
|
|
svc := newTestService(t, client)
|
|
cfg := importAliveConfig(t, svc)
|
|
ctx := context.Background()
|
|
|
|
if _, err := svc.ObjectContent(ctx, cfg.ID, "r", "b", ""); err == nil {
|
|
t.Error("空对象名应报错")
|
|
}
|
|
big := make([]byte, maxObjectPutBytes+1)
|
|
if _, err := svc.PutObjectContent(ctx, cfg.ID, "r", "b", "a", big, "", ""); !errors.Is(err, oci.ErrObjectTooLarge) {
|
|
t.Errorf("超限 PUT err = %v, want ErrObjectTooLarge", err)
|
|
}
|
|
}
|
|
|
|
func TestCreatePARDefaultsName(t *testing.T) {
|
|
client := &osStubClient{fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}}
|
|
svc := newTestService(t, client)
|
|
cfg := importAliveConfig(t, svc)
|
|
|
|
par, err := svc.CreatePAR(context.Background(), cfg.ID, "r", "b",
|
|
oci.CreatePARInput{ObjectName: "db/app.sql.gz", AccessType: "ObjectRead", ExpiresHours: 24})
|
|
if err != nil {
|
|
t.Fatalf("CreatePAR: %v", err)
|
|
}
|
|
if client.createdPAR.Name != "par-db-app.sql.gz" {
|
|
t.Errorf("default name = %q, want par-db-app.sql.gz", client.createdPAR.Name)
|
|
}
|
|
if par.FullURL == "" {
|
|
t.Errorf("FullURL empty, want populated")
|
|
}
|
|
}
|
|
|
|
// TestCreatePARAccessTypes 桶级 AnyObject* 必须放行(分享桶 500 复盘),非法类型拒绝。
|
|
func TestCreatePARAccessTypes(t *testing.T) {
|
|
client := &osStubClient{fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}}
|
|
svc := newTestService(t, client)
|
|
cfg := importAliveConfig(t, svc)
|
|
|
|
cases := []struct {
|
|
accessType string
|
|
wantErr bool
|
|
}{
|
|
{"AnyObjectRead", false}, {"AnyObjectWrite", false}, {"AnyObjectReadWrite", false},
|
|
{"BucketRead", true}, {"", true},
|
|
}
|
|
for _, tc := range cases {
|
|
_, err := svc.CreatePAR(context.Background(), cfg.ID, "r", "b",
|
|
oci.CreatePARInput{AccessType: tc.accessType, ExpiresHours: 24})
|
|
if (err != nil) != tc.wantErr {
|
|
t.Errorf("CreatePAR(%q) err = %v, wantErr %v", tc.accessType, err, tc.wantErr)
|
|
}
|
|
}
|
|
if client.createdPAR.Name != "par-bucket-b" {
|
|
t.Errorf("bucket par default name = %q, want par-bucket-b", client.createdPAR.Name)
|
|
}
|
|
}
|
|
|
|
func TestCreatePARValidationSentinels(t *testing.T) {
|
|
svc := &OciConfigService{}
|
|
cases := []struct {
|
|
name string
|
|
in oci.CreatePARInput
|
|
want error
|
|
}{
|
|
{"access type", oci.CreatePARInput{AccessType: "BucketRead", ExpiresHours: 1}, ErrPARInvalidAccessType},
|
|
{"expiration low", oci.CreatePARInput{AccessType: "ObjectRead", ExpiresHours: 0}, ErrPARInvalidExpiration},
|
|
{"expiration high", oci.CreatePARInput{AccessType: "ObjectRead", ExpiresHours: maxParHours + 1}, ErrPARInvalidExpiration},
|
|
}
|
|
for _, tc := range cases {
|
|
_, err := svc.CreatePAR(context.Background(), 1, "r", "b", tc.in)
|
|
if !errors.Is(err, tc.want) {
|
|
t.Errorf("%s err = %v, want errors.Is(%v)", tc.name, err, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDeleteBucketPurgeSingleflight 锁定同桶清空单飞:任务在途时重复删除请求
|
|
// 按已排队返回,不并发起第二个清空任务。
|
|
func TestDeleteBucketPurgeSingleflight(t *testing.T) {
|
|
client := &osStubClient{
|
|
fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}},
|
|
versions: []oci.ObjectVersion{{Name: "a.txt", VersionID: "v1"}},
|
|
}
|
|
svc := newTestService(t, client)
|
|
cfg := importAliveConfig(t, svc)
|
|
key := fmt.Sprintf("%d/r/b", cfg.ID)
|
|
if !svc.beginPurge(key) {
|
|
t.Fatal("beginPurge 首次抢占失败")
|
|
}
|
|
queued, err := svc.DeleteBucket(context.Background(), cfg.ID, "r", "b")
|
|
if err != nil || !queued {
|
|
t.Fatalf("在途时 DeleteBucket = queued %v, %v, want queued", queued, err)
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
client.mu.Lock()
|
|
calls := client.listCalls
|
|
client.mu.Unlock()
|
|
if calls != 0 {
|
|
t.Fatalf("在途期间起了新清空任务: listCalls=%d, want 0", calls)
|
|
}
|
|
svc.endPurge(key)
|
|
if !svc.beginPurge(key) {
|
|
t.Error("endPurge 后应可重新抢占")
|
|
}
|
|
svc.endPurge(key)
|
|
}
|
|
|
|
// TestDeleteBucketPurgeAbortsMultipart 锁定清空顺序包含中止未完成分片上传。
|
|
func TestDeleteBucketPurgeAbortsMultipart(t *testing.T) {
|
|
client := &osStubClient{
|
|
fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}},
|
|
versions: []oci.ObjectVersion{{Name: "a.txt", VersionID: "v1"}},
|
|
}
|
|
svc := newTestService(t, client)
|
|
cfg := importAliveConfig(t, svc)
|
|
if queued, err := svc.DeleteBucket(context.Background(), cfg.ID, "r", "b"); err != nil || !queued {
|
|
t.Fatalf("DeleteBucket = queued %v, %v, want queued", queued, err)
|
|
}
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
client.mu.Lock()
|
|
done := client.bucketDeleted && client.multipartAborted
|
|
client.mu.Unlock()
|
|
if done {
|
|
return
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
t.Fatal("purge 未在期限内完成分片中止与删桶")
|
|
}
|
|
|
|
// TestImportRejectsMissingProxy 锁定导入防线:关联不存在的代理直接拒绝,
|
|
// 不落库也不发起绕过代理的直连测活。
|
|
func TestImportRejectsMissingProxy(t *testing.T) {
|
|
svc := newTestService(t, &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}})
|
|
in := trialImportInput()
|
|
missing := uint(999)
|
|
in.ProxyID = &missing
|
|
if _, err := svc.Import(context.Background(), in); err == nil {
|
|
t.Fatal("Import 关联不存在的代理应报错")
|
|
}
|
|
var n int64
|
|
if err := svc.db.Model(&model.OciConfig{}).Count(&n).Error; err != nil || n != 0 {
|
|
t.Fatalf("拒绝导入后配置数 = %d (%v), want 0", n, err)
|
|
}
|
|
}
|