/*
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())
}