package service import ( "context" "strings" "sync" "testing" "time" "oci-portal/internal/oci" ) type reservedIPStubClient struct { *fakeClient mu sync.Mutex ips []oci.ReservedIP assigned map[string]string // publicIpId -> instanceId vnicBound map[string]string // publicIpId -> vnicId deleted []string instState string // GetInstance 返回的状态 launched oci.CreateInstanceInput } func (f *reservedIPStubClient) ListReservedIPs(ctx context.Context, cred oci.Credentials, region, compartmentID string) ([]oci.ReservedIP, error) { return f.ips, nil } func (f *reservedIPStubClient) CreateReservedIP(ctx context.Context, cred oci.Credentials, region, compartmentID, displayName string) (oci.ReservedIP, error) { ip := oci.ReservedIP{ID: "ocid1.publicip..new", DisplayName: displayName, IPAddress: "155.248.0.10", LifecycleState: "AVAILABLE"} f.ips = append(f.ips, ip) return ip, nil } func (f *reservedIPStubClient) AssignReservedIP(ctx context.Context, cred oci.Credentials, region, publicIPID, instanceID string) error { f.mu.Lock() defer f.mu.Unlock() if f.assigned == nil { f.assigned = map[string]string{} } f.assigned[publicIPID] = instanceID return nil } func (f *reservedIPStubClient) AssignReservedIPToVnic(ctx context.Context, cred oci.Credentials, region, publicIPID, vnicID string) error { f.mu.Lock() defer f.mu.Unlock() if f.vnicBound == nil { f.vnicBound = map[string]string{} } f.vnicBound[publicIPID] = vnicID return nil } func (f *reservedIPStubClient) DeleteReservedIP(ctx context.Context, cred oci.Credentials, region, publicIPID string) error { f.deleted = append(f.deleted, publicIPID) return nil } func (f *reservedIPStubClient) LaunchInstance(ctx context.Context, cred oci.Credentials, in oci.CreateInstanceInput) (oci.Instance, error) { f.launched = in return oci.Instance{ID: "ocid1.instance..new", LifecycleState: "PROVISIONING"}, nil } func (f *reservedIPStubClient) GetInstance(ctx context.Context, cred oci.Credentials, region, instanceID string) (oci.Instance, error) { return oci.Instance{ID: instanceID, LifecycleState: f.instState}, nil } func (f *reservedIPStubClient) assignedTo(publicIPID string) string { f.mu.Lock() defer f.mu.Unlock() return f.assigned[publicIPID] } func TestReservedIPCrud(t *testing.T) { client := &reservedIPStubClient{fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}} svc := newTestService(t, client) cfg := importAliveConfig(t, svc) ctx := context.Background() if _, err := svc.CreateReservedIP(ctx, cfg.ID, "ap-tokyo-1", "", "ip-1"); err != nil { t.Fatalf("CreateReservedIP: %v", err) } got, err := svc.ReservedIPs(ctx, cfg.ID, "ap-tokyo-1", "") if err != nil || len(got) != 1 || got[0].DisplayName != "ip-1" { t.Fatalf("ReservedIPs = %+v, %v", got, err) } // publicIpId 缺失被拒 if err := svc.AssignReservedIP(ctx, cfg.ID, "ap-tokyo-1", "", "inst", ""); err == nil || !strings.Contains(err.Error(), "publicIpId") { t.Errorf("AssignReservedIP 空 id err = %v, want publicIpId required", err) } if err := svc.AssignReservedIP(ctx, cfg.ID, "ap-tokyo-1", "ocid1.publicip..new", "ocid1.instance..a", ""); err != nil { t.Fatalf("AssignReservedIP: %v", err) } if got := client.assignedTo("ocid1.publicip..new"); got != "ocid1.instance..a" { t.Errorf("assigned = %q, want instance a", got) } // vnicId 非空时路由到按网卡绑定 if err := svc.AssignReservedIP(ctx, cfg.ID, "ap-tokyo-1", "ocid1.publicip..new", "", "ocid1.vnic..v1"); err != nil { t.Fatalf("AssignReservedIP vnic: %v", err) } if got := client.vnicBound["ocid1.publicip..new"]; got != "ocid1.vnic..v1" { t.Errorf("vnicBound = %q, want vnic v1", got) } if err := svc.DeleteReservedIP(ctx, cfg.ID, "ap-tokyo-1", "ocid1.publicip..new"); err != nil { t.Fatalf("DeleteReservedIP: %v", err) } if len(client.deleted) != 1 { t.Errorf("deleted = %v, want 1 entry", client.deleted) } } func TestCreateInstanceWithReservedIP(t *testing.T) { client := &reservedIPStubClient{ fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}, instState: "RUNNING", } svc := newTestService(t, client) cfg := importAliveConfig(t, svc) ctx := context.Background() in := oci.CreateInstanceInput{ AvailabilityDomain: "AD-1", DisplayName: "vm1", Shape: "VM.Standard.A1.Flex", ImageID: "ocid1.image..a", SubnetID: "ocid1.subnet..s", ReservedPublicIPID: "ocid1.publicip..r", } // 批量创建 + 保留 IP 被拒 if _, _, err := svc.CreateInstances(ctx, cfg.ID, in, 2); err == nil || !strings.Contains(err.Error(), "single instance") { t.Fatalf("批量+保留IP err = %v, want single instance", err) } instances, failures, err := svc.CreateInstances(ctx, cfg.ID, in, 1) if err != nil || len(failures) != 0 || len(instances) != 1 { t.Fatalf("CreateInstances = %+v, %v, %v", instances, failures, err) } // 后台 goroutine 轮询到 RUNNING 后完成绑定 deadline := time.Now().Add(3 * time.Second) for time.Now().Before(deadline) { if client.assignedTo("ocid1.publicip..r") == "ocid1.instance..new" { return } time.Sleep(20 * time.Millisecond) } t.Fatalf("assigned = %q, want bind to new instance", client.assignedTo("ocid1.publicip..r")) }