package service import ( "context" "testing" "time" "oci-portal/internal/oci" ) // consoleFakeClient 在 fakeClient 上覆写控制台连接三方法: // 首次 List 返回预置连接,删除后的后续 List 全部报告 DELETED。 type consoleFakeClient struct { fakeClient conns []oci.ConsoleConnection listed int deleted []string created int } func (f *consoleFakeClient) ListConsoleConnections(ctx context.Context, cred oci.Credentials, region, instanceID string) ([]oci.ConsoleConnection, error) { f.listed++ if f.listed == 1 { return f.conns, nil } gone := make([]oci.ConsoleConnection, len(f.conns)) for i, c := range f.conns { c.LifecycleState = "DELETED" gone[i] = c } return gone, nil } func (f *consoleFakeClient) DeleteConsoleConnection(ctx context.Context, cred oci.Credentials, region, connectionID string) error { f.deleted = append(f.deleted, connectionID) return nil } func (f *consoleFakeClient) CreateConsoleConnection(ctx context.Context, cred oci.Credentials, region, instanceID, sshPublicKey string) (oci.ConsoleConnection, error) { f.created++ return oci.ConsoleConnection{ ID: "ocid1.icc.new", InstanceID: instanceID, LifecycleState: "ACTIVE", VncConnectionString: "ssh -o ProxyCommand='ssh -W %h:%p -p 443 " + "ocid1.icc.new@instance-console.eu-frankfurt-1.oci.oraclecloud.com' -N -L ...", }, nil } func newConsoleTestService(t *testing.T, client *consoleFakeClient) (*ConsoleService, uint) { t.Helper() client.tenancy = oci.TenancyInfo{Name: "t"} svc := newTestService(t, client) cfg, err := svc.Import(context.Background(), trialImportInput()) if err != nil { t.Fatalf("import config: %v", err) } console := NewConsoleService(svc) console.pollInterval = time.Millisecond return console, cfg.ID } func TestCreateConsoleSession(t *testing.T) { tests := []struct { name string typ string conns []oci.ConsoleConnection wantErr bool wantDeleted int }{ {name: "非法类型拒绝", typ: "rdp", wantErr: true}, {name: "无残留直接创建", typ: ConsoleTypeSerial}, { name: "ACTIVE 残留先删再建", typ: ConsoleTypeVnc, conns: []oci.ConsoleConnection{ {ID: "ocid1.icc.stale", LifecycleState: "ACTIVE"}, }, wantDeleted: 1, }, { name: "DELETED 终态记录跳过", typ: ConsoleTypeSerial, conns: []oci.ConsoleConnection{ {ID: "ocid1.icc.gone", LifecycleState: "DELETED"}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client := &consoleFakeClient{conns: tt.conns} console, cfgID := newConsoleTestService(t, client) sess, err := console.CreateSession(context.Background(), cfgID, "ocid1.instance.i", "", tt.typ) if tt.wantErr { if err == nil { t.Fatal("expect error, got nil") } return } if err != nil { t.Fatalf("CreateSession: %v", err) } if sess.Type != tt.typ { t.Errorf("session type = %s, want %s", sess.Type, tt.typ) } if len(client.deleted) != tt.wantDeleted { t.Errorf("deleted %d stale connections, want %d", len(client.deleted), tt.wantDeleted) } if client.created != 1 { t.Errorf("created %d connections, want 1", client.created) } if console.Get(sess.ID) == nil { t.Error("session not retrievable after create") } }) } } // TestConsoleSessionExpire 验证过期回收:在用会话跳过本轮,断开后下一轮回收。 func TestConsoleSessionExpire(t *testing.T) { tests := []struct { name string inUse bool wantAlive bool }{ {name: "在用会话跳过回收", inUse: true, wantAlive: true}, {name: "空闲会话回收并删云端连接", inUse: false, wantAlive: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client := &consoleFakeClient{} console, cfgID := newConsoleTestService(t, client) sess, err := console.CreateSession(context.Background(), cfgID, "ocid1.instance.i", "", ConsoleTypeSerial) if err != nil { t.Fatalf("CreateSession: %v", err) } sess.MarkUse(tt.inUse) console.expire(sess.ID) if alive := console.Get(sess.ID) != nil; alive != tt.wantAlive { t.Fatalf("session alive = %v, want %v", alive, tt.wantAlive) } if wantDel := !tt.wantAlive; (len(client.deleted) == 1) != wantDel { t.Errorf("cloud connection deleted %v, want %v", client.deleted, wantDel) } if tt.inUse { // 断开后下一轮回收(此前的缺陷:跳过后不再检查,会话常驻) sess.MarkUse(false) console.expire(sess.ID) if console.Get(sess.ID) != nil { t.Fatal("session still alive after idle expire round") } if len(client.deleted) != 1 { t.Errorf("cloud connection not deleted after idle expire: %v", client.deleted) } } }) } }