package service import ( "context" "strings" "sync" "testing" "time" "oci-portal/internal/oci" ) // vnicStubClient 覆写 VNIC 三方法并记录透传参数,其余行为沿用 fakeClient。 type vnicStubClient struct { *fakeClient mu sync.Mutex vnics []oci.Vnic attached oci.AttachVnicInput attachInst string detachedID string ipv6Vnic string boundVnic string // AssignReservedIPToVnic 记录 boundIP string } func (f *vnicStubClient) ListInstanceVnics(ctx context.Context, cred oci.Credentials, region, instanceID string) ([]oci.Vnic, error) { f.mu.Lock() defer f.mu.Unlock() return f.vnics, nil } func (f *vnicStubClient) AssignReservedIPToVnic(ctx context.Context, cred oci.Credentials, region, publicIPID, vnicID string) error { f.mu.Lock() defer f.mu.Unlock() f.boundIP, f.boundVnic = publicIPID, vnicID return nil } func (f *vnicStubClient) bound() (string, string) { f.mu.Lock() defer f.mu.Unlock() return f.boundIP, f.boundVnic } func (f *vnicStubClient) AttachVnic(ctx context.Context, cred oci.Credentials, region, instanceID string, in oci.AttachVnicInput) (oci.Vnic, error) { f.attachInst, f.attached = instanceID, in return oci.Vnic{AttachmentID: "att-1", SubnetID: in.SubnetID, LifecycleState: "ATTACHING"}, nil } func (f *vnicStubClient) DetachVnic(ctx context.Context, cred oci.Credentials, region, attachmentID string) error { f.detachedID = attachmentID return nil } func (f *vnicStubClient) AddVnicIpv6(ctx context.Context, cred oci.Credentials, region, vnicID, address string) (string, error) { f.ipv6Vnic = vnicID return "2603:c022::1", nil } func TestVnicManagement(t *testing.T) { client := &vnicStubClient{ fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}, vnics: []oci.Vnic{{AttachmentID: "att-0", IsPrimary: true, PrivateIP: "10.0.0.2"}}, } svc := newTestService(t, client) cfg := importAliveConfig(t, svc) ctx := context.Background() got, err := svc.InstanceVnics(ctx, cfg.ID, "ap-tokyo-1", "ocid1.instance.oc1..x") if err != nil || len(got) != 1 || !got[0].IsPrimary { t.Fatalf("InstanceVnics = %+v, %v", got, err) } // subnetId 缺失被拒 _, err = svc.AttachVnic(ctx, cfg.ID, "ap-tokyo-1", "ocid1.instance.oc1..x", oci.AttachVnicInput{}) if err == nil || !strings.Contains(err.Error(), "subnetId") { t.Errorf("AttachVnic 无 subnet err = %v, want subnetId required", err) } in := oci.AttachVnicInput{SubnetID: "ocid1.subnet.oc1..s", AssignPublicIP: true, DisplayName: "vnic-2"} vnic, err := svc.AttachVnic(ctx, cfg.ID, "ap-tokyo-1", "ocid1.instance.oc1..x", in) if err != nil || vnic.AttachmentID != "att-1" || client.attached != in || client.attachInst != "ocid1.instance.oc1..x" { t.Fatalf("AttachVnic = %+v, %v (passed %+v)", vnic, err, client.attached) } if err := svc.DetachVnic(ctx, cfg.ID, "ap-tokyo-1", "att-1"); err != nil || client.detachedID != "att-1" { t.Fatalf("DetachVnic err = %v, detached = %q", err, client.detachedID) } // per-VNIC IPv6:vnicId 缺失被拒,正常路径透传 if _, err := svc.AddVnicIpv6(ctx, cfg.ID, "ap-tokyo-1", "", ""); err == nil { t.Error("AddVnicIpv6 空 vnicId 应报错") } addr, err := svc.AddVnicIpv6(ctx, cfg.ID, "ap-tokyo-1", "ocid1.vnic.oc1..v", "") if err != nil || addr == "" || client.ipv6Vnic != "ocid1.vnic.oc1..v" { t.Fatalf("AddVnicIpv6 = %q, %v (vnic %q)", addr, err, client.ipv6Vnic) } } func TestAttachVnicWithReservedIP(t *testing.T) { client := &vnicStubClient{ fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}, // 附加返回 att-1;列表里它已就绪,后台编排应绑定到对应 VNIC vnics: []oci.Vnic{{AttachmentID: "att-1", VnicID: "ocid1.vnic.oc1..new", LifecycleState: "ATTACHED"}}, } svc := newTestService(t, client) cfg := importAliveConfig(t, svc) in := oci.AttachVnicInput{SubnetID: "ocid1.subnet.oc1..s", ReservedPublicIPID: "ocid1.publicip..r"} if _, err := svc.AttachVnic(context.Background(), cfg.ID, "ap-tokyo-1", "ocid1.instance.oc1..x", in); err != nil { t.Fatalf("AttachVnic: %v", err) } deadline := time.Now().Add(3 * time.Second) for time.Now().Before(deadline) { if ip, vnic := client.bound(); ip == "ocid1.publicip..r" && vnic == "ocid1.vnic.oc1..new" { return } time.Sleep(20 * time.Millisecond) } ip, vnic := client.bound() t.Fatalf("bound = (%q, %q), want reserved ip bound to new vnic", ip, vnic) }