/* Copyright (c) 2017 VMware, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package simulator import ( "context" "crypto/tls" "crypto/x509" "errors" "io" "io/ioutil" "log" "net/http" "net/url" "reflect" "testing" "time" "github.com/vmware/govmomi" "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/simulator/esx" "github.com/vmware/govmomi/simulator/vpx" "github.com/vmware/govmomi/vim25" "github.com/vmware/govmomi/vim25/methods" "github.com/vmware/govmomi/vim25/mo" "github.com/vmware/govmomi/vim25/soap" "github.com/vmware/govmomi/vim25/types" ) func TestUnmarshal(t *testing.T) { requests := []struct { body interface{} data string }{ { &types.RetrieveServiceContent{ This: types.ManagedObjectReference{ Type: "ServiceInstance", Value: "ServiceInstance", }, }, ` <_this type="ServiceInstance">ServiceInstance `, }, { &types.Login{ This: types.ManagedObjectReference{ Type: "SessionManager", Value: "SessionManager", }, UserName: "root", Password: "secret", }, ` <_this type="SessionManager">SessionManager root secret `, }, { &types.RetrieveProperties{ This: types.ManagedObjectReference{Type: "PropertyCollector", Value: "ha-property-collector"}, SpecSet: []types.PropertyFilterSpec{ { DynamicData: types.DynamicData{}, PropSet: []types.PropertySpec{ { DynamicData: types.DynamicData{}, Type: "ManagedEntity", All: (*bool)(nil), PathSet: []string{"name", "parent"}, }, }, ObjectSet: []types.ObjectSpec{ { DynamicData: types.DynamicData{}, Obj: types.ManagedObjectReference{Type: "Folder", Value: "ha-folder-root"}, Skip: types.NewBool(false), SelectSet: []types.BaseSelectionSpec{ // test decode of interface &types.TraversalSpec{ SelectionSpec: types.SelectionSpec{ DynamicData: types.DynamicData{}, Name: "traverseParent", }, Type: "ManagedEntity", Path: "parent", Skip: types.NewBool(false), SelectSet: []types.BaseSelectionSpec{ &types.SelectionSpec{ DynamicData: types.DynamicData{}, Name: "traverseParent", }, }, }, }, }, }, ReportMissingObjectsInResults: (*bool)(nil), }, }}, ` <_this type="PropertyCollector">ha-property-collector ManagedEntity name parent ha-folder-root false traverseParent ManagedEntity parent false traverseParent `, }, } for i, req := range requests { method, err := UnmarshalBody(vim25MapType, []byte(req.data)) if err != nil { t.Errorf("failed to decode %d (%s): %s", i, req, err) } if !reflect.DeepEqual(method.Body, req.body) { t.Errorf("malformed body %d (%#v):", i, method.Body) } } } func TestUnmarshalError(t *testing.T) { requests := []string{ "", // io.EOF ` `, ` <_this type="ServiceInstance">ServiceInstance `, ` `, ` <_this type="ServiceInstance">ServiceInstance `, } for i, data := range requests { if _, err := UnmarshalBody(vim25MapType, []byte(data)); err != nil { continue } t.Errorf("expected %d (%s) to return an error", i, data) } } func TestServeHTTP(t *testing.T) { configs := []struct { content types.ServiceContent folder mo.Folder }{ {esx.ServiceContent, esx.RootFolder}, {vpx.ServiceContent, vpx.RootFolder}, } for _, config := range configs { s := New(NewServiceInstance(SpoofContext(), config.content, config.folder)) ts := s.NewServer() defer ts.Close() u := ts.URL.User ts.URL.User = nil ctx := context.Background() client, err := govmomi.NewClient(ctx, ts.URL, true) if err != nil { t.Fatal(err) } err = client.Login(ctx, nil) if err == nil { t.Fatal("expected invalid login error") } err = client.Login(ctx, u) if err != nil { t.Fatal(err) } // Testing http client + reflect client clients := []soap.RoundTripper{client, s.client} for _, c := range clients { now, err := methods.GetCurrentTime(ctx, c) if err != nil { t.Fatal(err) } if now.After(time.Now()) { t.Fail() } // test the fail/Fault path _, err = methods.QueryVMotionCompatibility(ctx, c, &types.QueryVMotionCompatibility{}) if err == nil { t.Errorf("expected error") } } err = client.Logout(ctx) if err != nil { t.Error(err) } } } func TestServeAbout(t *testing.T) { ctx := context.Background() m := VPX() m.App = 1 m.Pod = 1 defer m.Remove() err := m.Create() if err != nil { t.Fatal(err) } s := m.Service.NewServer() defer s.Close() c, err := govmomi.NewClient(ctx, s.URL, true) if err != nil { t.Fatal(err) } u := *s.URL u.Path += "/vimServiceVersions.xml" r, err := c.Get(u.String()) if err != nil { t.Fatal(err) } _ = r.Body.Close() u.Path = "/about" r, err = c.Get(u.String()) if err != nil { t.Fatal(err) } _ = r.Body.Close() } func TestServeHTTPS(t *testing.T) { s := New(NewServiceInstance(SpoofContext(), esx.ServiceContent, esx.RootFolder)) s.TLS = new(tls.Config) ts := s.NewServer() defer ts.Close() ts.Config.ErrorLog = log.New(ioutil.Discard, "", 0) // silence benign "TLS handshake error" log messages ctx := context.Background() // insecure=true OK _, err := govmomi.NewClient(ctx, ts.URL, true) if err != nil { t.Fatal(err) } // insecure=false should FAIL _, err = govmomi.NewClient(ctx, ts.URL, false) if err == nil { t.Fatal("expected error") } uerr, ok := err.(*url.Error) if !ok { t.Fatalf("err type=%T", err) } _, ok = uerr.Err.(x509.UnknownAuthorityError) if !ok { t.Fatalf("err type=%T", uerr.Err) } sinfo := ts.CertificateInfo() // Test thumbprint validation sc := soap.NewClient(ts.URL, false) // Add host with thumbprint mismatch should fail sc.SetThumbprint(ts.URL.Host, "nope") _, err = vim25.NewClient(ctx, sc) if err == nil { t.Error("expected error") } // Add host with thumbprint match should pass sc.SetThumbprint(ts.URL.Host, sinfo.ThumbprintSHA1) _, err = vim25.NewClient(ctx, sc) if err != nil { t.Fatal(err) } var pinfo object.HostCertificateInfo err = pinfo.FromURL(ts.URL, nil) if err != nil { t.Fatal(err) } if pinfo.ThumbprintSHA1 != sinfo.ThumbprintSHA1 { t.Error("thumbprint mismatch") } // Test custom RootCAs list sc = soap.NewClient(ts.URL, false) caFile, err := ts.CertificateFile() if err != nil { t.Fatal(err) } if err = sc.SetRootCAs(caFile); err != nil { t.Fatal(err) } _, err = vim25.NewClient(ctx, sc) if err != nil { t.Fatal(err) } } type errorMarshal struct { mo.ServiceInstance } func (*errorMarshal) Fault() *soap.Fault { return nil } func (*errorMarshal) MarshalText() ([]byte, error) { return nil, errors.New("time has stopped") } func (h *errorMarshal) CurrentTime(types.AnyType) soap.HasFault { return h } type errorNoSuchMethod struct { mo.ServiceInstance } func TestServeHTTPErrors(t *testing.T) { s := New(NewServiceInstance(SpoofContext(), esx.ServiceContent, esx.RootFolder)) ts := s.NewServer() defer ts.Close() ctx := context.Background() client, err := govmomi.NewClient(ctx, ts.URL, true) if err != nil { t.Fatal(err) } // test response to unimplemented method req := &types.QueryMemoryOverhead{This: esx.HostSystem.Reference()} _, err = methods.QueryMemoryOverhead(ctx, client.Client, req) if _, ok := soap.ToSoapFault(err).VimFault().(types.MethodNotFound); !ok { t.Error("expected MethodNotFound fault") } // cover the does not implement method error path Map.objects[vim25.ServiceInstance] = &errorNoSuchMethod{} _, err = methods.GetCurrentTime(ctx, client) if err == nil { t.Error("expected error") } // cover the xml encode error path Map.objects[vim25.ServiceInstance] = &errorMarshal{} _, err = methods.GetCurrentTime(ctx, client) if err == nil { t.Error("expected error") } // cover the no such object path Map.Remove(SpoofContext(), vim25.ServiceInstance) _, err = methods.GetCurrentTime(ctx, client) if err == nil { t.Error("expected error") } // verify we properly marshal the fault fault := soap.ToSoapFault(err).VimFault() f, ok := fault.(types.ManagedObjectNotFound) if !ok { t.Fatalf("fault=%#v", fault) } if f.Obj != vim25.ServiceInstance { t.Errorf("obj=%#v", f.Obj) } // cover the method not supported path res, err := http.Get(ts.URL.String()) if err != nil { log.Fatal(err) } if res.StatusCode != http.StatusMethodNotAllowed { t.Errorf("expected status %d, got %s", http.StatusMethodNotAllowed, res.Status) } // cover the ioutil.ReadAll error path s.readAll = func(io.Reader) ([]byte, error) { return nil, io.ErrShortBuffer } res, err = http.Post(ts.URL.String(), "none", nil) if err != nil { log.Fatal(err) } if res.StatusCode != http.StatusBadRequest { t.Errorf("expected status %d, got %s", http.StatusBadRequest, res.Status) } } func TestDelay(t *testing.T) { m := ESX() defer m.Remove() err := m.Create() if err != nil { t.Fatal(err) } s := m.Service.NewServer() defer s.Close() client, err := govmomi.NewClient(context.Background(), s.URL, true) if err != nil { t.Fatal(err) } simvm := Map.Any("VirtualMachine").(*VirtualMachine) vm := object.NewVirtualMachine(client.Client, simvm.Reference()) m.Service.delay.Delay = 1000 ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) defer cancel() _, err = vm.PowerOff(ctx) if err == nil { t.Fatalf("expected timeout initiating task") } // give time for task to finish time.Sleep(1000 * time.Millisecond) } func TestDelayTask(t *testing.T) { m := ESX() defer m.Remove() err := m.Create() if err != nil { t.Fatal(err) } s := m.Service.NewServer() defer s.Close() client, err := govmomi.NewClient(context.Background(), s.URL, true) if err != nil { t.Fatal(err) } simvm := Map.Any("VirtualMachine").(*VirtualMachine) vm := object.NewVirtualMachine(client.Client, simvm.Reference()) TaskDelay.Delay = 1000 defer func() { TaskDelay.Delay = 0 }() task, err := vm.PowerOff(context.Background()) if err != nil { t.Fatal(err) } timeoutCtx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) defer cancel() err = task.Wait(timeoutCtx) if err == nil { t.Fatal("expected timeout waiting for task") } // make sure to wait for task, or else it can run while other tests run! task.Wait(context.Background()) }