Repository: vmware/govmomi
Pull request #2734 (2022-02-02 11:56:48)

fix: switch url encoding for namespace.cluster.enable edge cluster lookup
## Description

Was receiving an error when running namespace.cluster.enable (with Vcenter 7U2):

```
govc: error in compatible switch lookup: GET https://VCENTER-HOST/api/vcenter/namespace-management/edge-cluster-compatibility?cluster=domain-c8&compatible=true&distributed_switch=50+32+27+0c+a8+8e+db+57-33+db+9e+e7+6e+49+58+ca: 500 Internal Server Error
```

It seems like vCenter is no longer accepting the parameter strings query-encoded (" " -> "+") and instead wants them to be URL-encoded (" " -> "%20"). Both ways should be valid, but only the second is now accepted. This isn't how go encodes strings, so it requires a bit of working-around.

Also noticed that the example given in the help no longer works, an additional mandatory parameter is needed

## Type of change

Please mark options that are relevant:

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to
  not work as expected)
- [ ] This change requires a documentation update
- [ ] Build related change

## How Has This Been Tested?

Added unit tests around expected built URLs. Tested the command E2E manually against my VCenter 7 U2

## Checklist:

- [x] My code follows the CONTRIBUTION [guidelines](./../CONTRIBUTING.md) of
  this project
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
  -> Seems like I have issues running the example client, apparently unrelated to this change. Same error both on master branch and my branch:
```
    317-ok          github.com/vmware/govmomi/guest 1.054s
318-=== RUN   ExampleClient_Run
319:2022/02/02 12:33:44 DC0_H0_VM0 [/bin/sh -c docker run -d --name vcsim-DC0_H0_VM0-265104de-1472-547c-b873-6dc7883fb6cb -v vcsim-DC0_H0_VM0-265104de-1472-547c-b873-6dc7883fb6cb:/sys/class/dmi/id:ro nginx]: exit status 125 docker: Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: process_linux.go:545: container init caused: rootfs_linux.go:76: mounting "/var/lib/docker/volumes/vcsim-DC0_H0_VM0-265104de-1472-547c-b873-6dc7883fb6cb/_data" to rootfs at "/sys/class/dmi/id" caused: mkdir /var/lib/docker/overlay2/9b0e1a6f265917d62b00f9c03d9acdbed8901c6e9693186299b6b12554f89c24/merged/sys/class/dmi: read-only file system: unknown.
320:--- FAIL: ExampleClient_Run (4.08s)
321-panic: ServerFaultCode: GuestOperationsUnavailable [recovered]
322-    panic: ServerFaultCode: GuestOperationsUnavailable
323-
--
340-    /home/linuxbrew/.linuxbrew/Cellar/go/1.17.6/libexec/src/testing/testing.go:1505 +0xa5d
341-main.main()
342-    _testmain.go:45 +0x22c
343:FAIL        github.com/vmware/govmomi/guest/toolbox 4.110s
344-?           github.com/vmware/govmomi/history       [no test files]
345-=== RUN   TestHostSystemManagementIPs
346---- PASS: TestHostSystemManagementIPs (0.00s)
--
539-=== RUN   TestRefreshDatastore
540---- PASS: TestRefreshDatastore (0.00s)
541-=== RUN   TestDatastoreHTTP
542:2022/02/02 12:33:45 failed to PUT '/tmp/govcsim-ha-datacenter-LocalDS_0-3576033707': open /tmp/govcsim-ha-datacenter-LocalDS_0-3576033707: is a directory
543:2022/02/02 12:33:46 failed to locate datastore with query params: dsName=nope
544:2022/02/02 12:33:47 failed to PUT '/tmp/govcsim-DC0-LocalDS_0-3304178270': open /tmp/govcsim-DC0-LocalDS_0-3304178270: is a directory
545:2022/02/02 12:33:47 failed to locate datastore with query params: dsName=nope
546---- PASS: TestDatastoreHTTP (2.67s)
547-=== RUN   TestDVS
548---- PASS: TestDVS (0.77s)
--
3413-2022/02/02 12:34:50 [VmInstanceUuidAssignedEvent] Assign a new instance UUID (a9a58383-ffb4-5c09-b5e0-cd19153b9e91) to nginx
3414-2022/02/02 12:34:50 [VmUuidAssignedEvent] Assigned new BIOS UUID (9d949740-e0e2-5783-903b-a7024c36273d) to nginx on DC0_H0 in DC0
3415-2022/02/02 12:34:50 [VmCreatedEvent] Created virtual machine nginx on DC0_H0 in DC0
3416:2022/02/02 12:34:54 nginx [/bin/sh -c docker run -d --name vcsim-nginx-9d949740-e0e2-5783-903b-a7024c36273d -v vcsim-nginx-9d949740-e0e2-5783-903b-a7024c36273d:/sys/class/dmi/id:ro -v '/tmp/example2457113515:/usr/share/nginx/html:ro' nginx]: exit status 125 docker: Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: process_linux.go:545: container init caused: rootfs_linux.go:76: mounting "/var/lib/docker/volumes/vcsim-nginx-9d949740-e0e2-5783-903b-a7024c36273d/_data" to rootfs at "/sys/class/dmi/id" caused: mkdir /var/lib/docker/overlay2/eb24e4235b988df13cbe2e148b2a28b2e50a0bea786087e08e60e34448c976d4/merged/sys/class/dmi: read-only file system: unknown.
3417-2022/02/02 12:34:54 [VmStartingEvent] nginx on host DC0_H0 in DC0 is starting
3418-2022/02/02 12:34:54 [VmPoweredOnEvent] nginx on DC0_H0 in DC0 is powered on
3419-panic: test timed out after 5m0s
```
- [x] Any dependent changes have been merged

embano1: Do we also need to touch the simulator related packages, e.g. https://github.com/vmware/govmomi/blob/742f2893eb7f408574ef0fe9882ca732e7e63651/vapi/namespace/simulator/simulator.go#L184?
benjvi: Since the simulator methods are more concerned with the response than the requests, I would say its OK as-is. Thee same logic about escaping won't come into play, because the relevant values only get returned as part of the response body - rather than in the URL
embano1: LGTM, can you please squash and clean up the commits?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a05f1275..ce2ec3b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,20 +1,4 @@
 
-<a name="v0.27.3"></a>
-## [Release v0.27.3](https://github.com/vmware/govmomi/compare/v0.27.2...v0.27.3)
-
-> Release Date: 2022-02-01
-
-### 🐞 Fix
-
-- [2d7cd133]	Add IPv6 support for signing HTTP request
-
-### ⚠️ BREAKING
-
-### 📖 Commits
-
-- [6b4e2391]	sts: support issuing HoK token using HoK token
-- [2d7cd133]	fix: Add IPv6 support for signing HTTP request
-
 <a name="v0.27.2"></a>
 ## [Release v0.27.2](https://github.com/vmware/govmomi/compare/v0.27.1...v0.27.2)
 
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6af4f695..37d679b2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -109,34 +109,30 @@ $ git push --force-with-lease $USER issue-<number>
 
 ### Updating Pull Requests
 
-If your PR fails to pass CI or needs changes based on code review, it's ok to
-add more commits stating the changes made, e.g. "Address review comments". This
-is to assist the reviewer(s) to easily detect and review the recent changes.
+If your PR fails to pass CI or needs changes based on code review, you'll most
+likely want to squash these changes into existing commits.
 
-In case of small PRs, it's ok to squash and force-push (see further below)
-directly instead.
+If your pull request contains a single commit or your changes are related to the
+most recent commit, you can simply amend the commit.
 
 ```console
-# incorporate review feedback
 $ git add .
-
-# create a fixup commit which will be merged into your (original) <commit>
-$ git commit --fixup <commit>
-$ git push $USER issue-<number>
+$ git commit --amend
+$ git push --force-with-lease $USER issue-<number>
 ```
 
-Be sure to add a comment to the PR indicating your new changes are ready to
-review, as Github does not generate a notification when you git push.
-
-Once the review is complete, squash and push your final commit(s):
+If you need to squash changes into an earlier commit, you can use:
 
 ```console
-# squash all commits into one
-# --autosquash will automatically detect and merge fixup commits
+$ git add .
+$ git commit --fixup <commit>
 $ git rebase -i --autosquash vmware/master
 $ git push --force-with-lease $USER issue-<number>
 ```
 
+Be sure to add a comment to the PR indicating your new changes are ready to
+review, as Github does not generate a notification when you git push.
+
 ### Code Style
 
 The coding style suggested by the Go community is used in `govmomi`. See the
diff --git a/govc/USAGE.md b/govc/USAGE.md
index 463b8297..8dd3d957 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -270,7 +270,6 @@ but appear via `govc $cmd -h`:
  - [sso.group.ls](#ssogroupls)
  - [sso.group.rm](#ssogrouprm)
  - [sso.group.update](#ssogroupupdate)
- - [sso.idp.ls](#ssoidpls)
  - [sso.service.ls](#ssoservicels)
  - [sso.user.create](#ssousercreate)
  - [sso.user.id](#ssouserid)
@@ -3722,6 +3721,7 @@ Examples:
     -cluster "Workload-Cluster" \
     -service-cidr 10.96.0.0/23 \
     -pod-cidrs 10.244.0.0/20 \
+    -control-plane-dns 10.10.10.10 \
     -control-plane-dns-names wcp.example.com \
     -workload-network.egress-cidrs 10.0.0.128/26 \
     -workload-network.ingress-cidrs "10.0.0.64/26" \
@@ -4569,20 +4569,6 @@ Options:
   -r=                    Remove user/group from group
 ```
 
-## sso.idp.ls
-
-```
-Usage: govc sso.idp.ls [OPTIONS]
-
-List SSO identity provider sources.
-
-Examples:
-  govc sso.idp.ls
-  govc sso.idp.ls -json
-
-Options:
-```
-
 ## sso.service.ls
 
 ```
diff --git a/govc/importx/spec.go b/govc/importx/spec.go
index e425fe62..514f338c 100644
--- a/govc/importx/spec.go
+++ b/govc/importx/spec.go
@@ -107,7 +107,7 @@ func (cmd *spec) Run(ctx context.Context, f *flag.FlagSet) error {
 			return fmt.Errorf("invalid file extension %s", path.Ext(fpath))
 		}
 
-		if isRemotePath(f.Arg(0)) {
+		if isRemotePath(fpath) {
 			client, err := cmd.Client()
 			if err != nil {
 				return err
diff --git a/govc/main.go b/govc/main.go
index 28451bce..5c6768a8 100644
--- a/govc/main.go
+++ b/govc/main.go
@@ -80,7 +80,6 @@ import (
 	_ "github.com/vmware/govmomi/govc/role"
 	_ "github.com/vmware/govmomi/govc/session"
 	_ "github.com/vmware/govmomi/govc/sso/group"
-	_ "github.com/vmware/govmomi/govc/sso/idp"
 	_ "github.com/vmware/govmomi/govc/sso/service"
 	_ "github.com/vmware/govmomi/govc/sso/user"
 	_ "github.com/vmware/govmomi/govc/storage/policy"
diff --git a/govc/namespace/cluster/enable.go b/govc/namespace/cluster/enable.go
index b67b6722..8e5797bf 100644
--- a/govc/namespace/cluster/enable.go
+++ b/govc/namespace/cluster/enable.go
@@ -154,6 +154,7 @@ Examples:
     -cluster "Workload-Cluster" \
     -service-cidr 10.96.0.0/23 \
     -pod-cidrs 10.244.0.0/20 \
+    -control-plane-dns 10.10.10.10 \
     -control-plane-dns-names wcp.example.com \
     -workload-network.egress-cidrs 10.0.0.128/26 \
     -workload-network.ingress-cidrs "10.0.0.64/26" \
@@ -243,7 +244,7 @@ func (cmd *enableCluster) Run(ctx context.Context, f *flag.FlagSet) error {
 	edgeClusterDisplayName := cmd.NcpClusterNetworkSpec.NsxEdgeCluster
 	edgeClusters, err := m.ListCompatibleEdgeClusters(ctx, clusterId, switchId)
 	if err != nil {
-		return fmt.Errorf("error in compatible switch lookup: %s", err)
+		return fmt.Errorf("error in compatible edge cluster lookup: %s", err)
 	}
 
 	matchingEdgeClusters := make([]string, 0)
diff --git a/govc/session/login.go b/govc/session/login.go
index 87e72078..2d0bbeca 100644
--- a/govc/session/login.go
+++ b/govc/session/login.go
@@ -215,13 +215,6 @@ func (cmd *login) loginByToken(ctx context.Context, c *vim25.Client) error {
 		},
 	}
 
-	// something behind the LoginByToken scene requires a version from /sdk/vimServiceVersions.xml
-	// in the SOAPAction header. For example, if vim25.Version is "7.0" but the service version is "6.3",
-	// LoginByToken fails with: 'VersionMismatchFaultCode: Unsupported version URI "urn:vim25/7.0"'
-	if c.Version == vim25.Version {
-		_ = c.UseServiceVersion()
-	}
-
 	return session.NewManager(c).LoginByToken(c.WithHeader(ctx, header))
 }
 
diff --git a/govc/sso/idp/ls.go b/govc/sso/idp/ls.go
deleted file mode 100644
index 1d75355b..00000000
--- a/govc/sso/idp/ls.go
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
-Copyright (c) 2022 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 idp
-
-import (
-	"context"
-	"flag"
-	"fmt"
-	"io"
-	"strings"
-	"text/tabwriter"
-
-	"github.com/vmware/govmomi/govc/cli"
-	"github.com/vmware/govmomi/govc/flags"
-	"github.com/vmware/govmomi/govc/sso"
-	"github.com/vmware/govmomi/ssoadmin"
-	"github.com/vmware/govmomi/ssoadmin/types"
-)
-
-type ls struct {
-	*flags.ClientFlag
-	*flags.OutputFlag
-}
-
-func init() {
-	cli.Register("sso.idp.ls", &ls{})
-}
-
-func (cmd *ls) Register(ctx context.Context, f *flag.FlagSet) {
-	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
-	cmd.ClientFlag.Register(ctx, f)
-
-	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
-	cmd.OutputFlag.Register(ctx, f)
-}
-
-func (cmd *ls) Description() string {
-	return `List SSO identity provider sources.
-
-Examples:
-  govc sso.idp.ls
-  govc sso.idp.ls -json`
-}
-
-func (cmd *ls) Process(ctx context.Context) error {
-	if err := cmd.ClientFlag.Process(ctx); err != nil {
-		return err
-	}
-	return cmd.OutputFlag.Process(ctx)
-}
-
-type idpInfo struct {
-	IdentitySources *types.IdentitySources
-}
-
-func (r *idpInfo) Write(w io.Writer) error {
-	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
-
-	type entry struct {
-		name   string
-		url    string
-		kind   string
-		domain string
-		alias  string
-	}
-
-	var entries []entry
-
-	for _, domain := range r.IdentitySources.System.Domains {
-		entries = append(entries, entry{"-", "-", "System Domain", domain.Name, domain.Alias})
-	}
-
-	if r.IdentitySources.LocalOS != nil {
-		for _, domain := range r.IdentitySources.LocalOS.Domains {
-			entries = append(entries, entry{"-", "-", "Local OS", domain.Name, domain.Alias})
-		}
-	}
-
-	if ad := r.IdentitySources.NativeAD; ad != nil {
-		for _, domain := range ad.Domains {
-			entries = append(entries, entry{ad.Name, "-", "Active Directory", domain.Name, domain.Alias})
-		}
-	}
-
-	for _, ldap := range r.IdentitySources.LDAPS {
-		for _, domain := range ldap.Domains {
-			entries = append(entries, entry{ldap.Name, ldap.Details.PrimaryURL, ldap.Type, domain.Name, domain.Alias})
-		}
-	}
-
-	fmt.Fprintln(tw, "Name\tServer URL\tType\tDomain\tAlias")
-
-	for _, e := range entries {
-		fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", e.name, e.url, e.kind, strings.ToLower(e.domain), e.alias)
-	}
-
-	return tw.Flush()
-}
-
-func (cmd *ls) Run(ctx context.Context, f *flag.FlagSet) error {
-	return sso.WithClient(ctx, cmd.ClientFlag, func(c *ssoadmin.Client) error {
-		sources, err := c.IdentitySources(ctx)
-		if err != nil {
-			return err
-		}
-
-		return cmd.WriteResult(&idpInfo{sources})
-	})
-}
diff --git a/simulator/dvs.go b/simulator/dvs.go
index 508852a4..05216ef0 100644
--- a/simulator/dvs.go
+++ b/simulator/dvs.go
@@ -277,12 +277,18 @@ func (s *DistributedVirtualSwitch) dvPortgroups(_ *types.DistributedVirtualSwitc
 
 	for _, ref := range s.Portgroup {
 		pg := Map.Get(ref).(*DistributedVirtualPortgroup)
+		res = append(res, types.DistributedVirtualPort{
+			DvsUuid: s.Uuid,
+			Key:     pg.Key,
+			Config: types.DVPortConfigInfo{
+				Setting: pg.Config.DefaultPortConfig,
+			},
+		})
 
 		for _, key := range pg.PortKeys {
 			res = append(res, types.DistributedVirtualPort{
-				DvsUuid:      s.Uuid,
-				Key:          key,
-				PortgroupKey: pg.Key,
+				DvsUuid: s.Uuid,
+				Key:     key,
 				Config: types.DVPortConfigInfo{
 					Setting: pg.Config.DefaultPortConfig,
 				},
diff --git a/simulator/folder.go b/simulator/folder.go
index ac93249b..a329112f 100644
--- a/simulator/folder.go
+++ b/simulator/folder.go
@@ -20,7 +20,6 @@ import (
 	"errors"
 	"fmt"
 	"math/rand"
-	"net/url"
 	"path"
 	"strings"
 
@@ -199,11 +198,9 @@ func (f *Folder) CreateFolder(ctx *Context, c *types.CreateFolder) soap.HasFault
 	r := &methods.CreateFolderBody{}
 
 	if folderHasChildType(&f.Folder, "Folder") {
-		name := f.escapeSpecialCharacters(c.Name)
-
-		if obj := ctx.Map.FindByName(name, f.ChildEntity); obj != nil {
+		if obj := ctx.Map.FindByName(c.Name, f.ChildEntity); obj != nil {
 			r.Fault_ = Fault("", &types.DuplicateName{
-				Name:   name,
+				Name:   c.Name,
 				Object: f.Self,
 			})
 
@@ -212,7 +209,7 @@ func (f *Folder) CreateFolder(ctx *Context, c *types.CreateFolder) soap.HasFault
 
 		folder := &Folder{}
 
-		folder.Name = name
+		folder.Name = c.Name
 		folder.ChildType = f.ChildType
 
 		folderPutChild(ctx, &f.Folder, folder)
@@ -227,13 +224,6 @@ func (f *Folder) CreateFolder(ctx *Context, c *types.CreateFolder) soap.HasFault
 	return r
 }
 
-func (f *Folder) escapeSpecialCharacters(name string) string {
-	name = strings.ReplaceAll(name, `%`, strings.ToLower(url.QueryEscape(`%`)))
-	name = strings.ReplaceAll(name, `/`, strings.ToLower(url.QueryEscape(`/`)))
-	name = strings.ReplaceAll(name, `\`, strings.ToLower(url.QueryEscape(`\`)))
-	return name
-}
-
 // StoragePod aka "Datastore Cluster"
 type StoragePod struct {
 	mo.StoragePod
@@ -640,9 +630,8 @@ func (f *Folder) CreateDVSTask(ctx *Context, req *types.CreateDVS_Task) soap.Has
 
 		dvs.AddDVPortgroupTask(ctx, &types.AddDVPortgroup_Task{
 			Spec: []types.DVPortgroupConfigSpec{{
-				Name:     dvs.Name + "-DVUplinks" + strings.TrimPrefix(dvs.Self.Value, "dvs"),
-				Type:     string(types.DistributedVirtualPortgroupPortgroupTypeEarlyBinding),
-				NumPorts: 1,
+				Name: dvs.Name + "-DVUplinks" + strings.TrimPrefix(dvs.Self.Value, "dvs"),
+				Type: string(types.DistributedVirtualPortgroupPortgroupTypeEarlyBinding),
 				DefaultPortConfig: &types.VMwareDVSPortSetting{
 					Vlan: &types.VmwareDistributedVirtualSwitchTrunkVlanSpec{
 						VlanId: []types.NumericRange{{Start: 0, End: 4094}},
diff --git a/simulator/model.go b/simulator/model.go
index d6ace75e..594ed137 100644
--- a/simulator/model.go
+++ b/simulator/model.go
@@ -654,9 +654,8 @@ func (m *Model) Create() error {
 		for npg := 0; npg < m.Portgroup; npg++ {
 			name := m.fmtName(dcName+"_DVPG", npg)
 			spec := types.DVPortgroupConfigSpec{
-				Name:     name,
-				Type:     string(types.DistributedVirtualPortgroupPortgroupTypeEarlyBinding),
-				NumPorts: 1,
+				Name: name,
+				Type: string(types.DistributedVirtualPortgroupPortgroupTypeEarlyBinding),
 			}
 
 			task, err := dvs.AddPortgroup(ctx, []types.DVPortgroupConfigSpec{spec})
diff --git a/ssoadmin/client.go b/ssoadmin/client.go
index 88f7d08f..1a03d1e5 100644
--- a/ssoadmin/client.go
+++ b/ssoadmin/client.go
@@ -441,16 +441,3 @@ func (c *Client) RevokeWSTrustRole(ctx context.Context, id types.PrincipalId, ro
 
 	return res.Returnval, nil
 }
-
-func (c *Client) IdentitySources(ctx context.Context) (*types.IdentitySources, error) {
-	req := types.Get{
-		This: c.ServiceContent.IdentitySourceManagementService,
-	}
-
-	res, err := methods.Get(ctx, c, &req)
-	if err != nil {
-		return nil, err
-	}
-
-	return &res.Returnval, nil
-}
diff --git a/ssoadmin/methods/methods.go b/ssoadmin/methods/methods.go
index 8705adff..180a4d48 100644
--- a/ssoadmin/methods/methods.go
+++ b/ssoadmin/methods/methods.go
@@ -743,26 +743,6 @@ func FindUsersInGroup(ctx context.Context, r soap.RoundTripper, req *types.FindU
 	return resBody.Res, nil
 }
 
-type GetBody struct {
-	Req    *types.Get         `xml:"urn:sso Get,omitempty"`
-	Res    *types.GetResponse `xml:"urn:sso GetResponse,omitempty"`
-	Fault_ *soap.Fault        `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault,omitempty"`
-}
-
-func (b *GetBody) Fault() *soap.Fault { return b.Fault_ }
-
-func Get(ctx context.Context, r soap.RoundTripper, req *types.Get) (*types.GetResponse, error) {
-	var reqBody, resBody GetBody
-
-	reqBody.Req = req
-
-	if err := r.RoundTrip(ctx, &reqBody, &resBody); err != nil {
-		return nil, err
-	}
-
-	return resBody.Res, nil
-}
-
 type GetAllCertificatesBody struct {
 	Req    *types.GetAllCertificates         `xml:"urn:sso GetAllCertificates,omitempty"`
 	Res    *types.GetAllCertificatesResponse `xml:"urn:sso GetAllCertificatesResponse,omitempty"`
diff --git a/ssoadmin/simulator/simulator.go b/ssoadmin/simulator/simulator.go
deleted file mode 100644
index e4564840..00000000
--- a/ssoadmin/simulator/simulator.go
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
-   Copyright (c) 2022 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 (
-	"github.com/vmware/govmomi/simulator"
-	"github.com/vmware/govmomi/ssoadmin"
-	"github.com/vmware/govmomi/ssoadmin/methods"
-	"github.com/vmware/govmomi/ssoadmin/types"
-	"github.com/vmware/govmomi/vim25/soap"
-	vim "github.com/vmware/govmomi/vim25/types"
-)
-
-func init() {
-	simulator.RegisterEndpoint(func(s *simulator.Service, r *simulator.Registry) {
-		if r.IsVPX() {
-			s.RegisterSDK(New())
-		}
-	})
-}
-
-var content = types.AdminServiceContent{
-	SessionManager:                  vim.ManagedObjectReference{Type: "SsoSessionManager", Value: "ssoSessionManager"},
-	ConfigurationManagementService:  vim.ManagedObjectReference{Type: "SsoAdminConfigurationManagementService", Value: "configurationManagementService"},
-	SmtpManagementService:           vim.ManagedObjectReference{Type: "SsoAdminSmtpManagementService", Value: "smtpManagementService"},
-	PrincipalDiscoveryService:       vim.ManagedObjectReference{Type: "SsoAdminPrincipalDiscoveryService", Value: "principalDiscoveryService"},
-	PrincipalManagementService:      vim.ManagedObjectReference{Type: "SsoAdminPrincipalManagementService", Value: "principalManagementService"},
-	RoleManagementService:           vim.ManagedObjectReference{Type: "SsoAdminRoleManagementService", Value: "roleManagementService"},
-	PasswordPolicyService:           vim.ManagedObjectReference{Type: "SsoAdminPasswordPolicyService", Value: "passwordPolicyService"},
-	LockoutPolicyService:            vim.ManagedObjectReference{Type: "SsoAdminLockoutPolicyService", Value: "lockoutPolicyService"},
-	DomainManagementService:         vim.ManagedObjectReference{Type: "SsoAdminDomainManagementService", Value: "domainManagementService"},
-	IdentitySourceManagementService: vim.ManagedObjectReference{Type: "SsoAdminIdentitySourceManagementService", Value: "identitySourceManagementService"},
-	SystemManagementService:         vim.ManagedObjectReference{Type: "SsoAdminSystemManagementService", Value: "systemManagementService"},
-	DeploymentInformationService:    vim.ManagedObjectReference{Type: "SsoAdminDeploymentInformationService", Value: "deploymentInformationService"},
-	ReplicationService:              vim.ManagedObjectReference{Type: "SsoAdminReplicationService", Value: "replicationService"},
-}
-
-var groupcheckServiceInstance = vim.ManagedObjectReference{
-	Type: "SsoGroupcheckServiceInstance", Value: "ServiceInstance",
-}
-
-var IdentitySources = types.IdentitySources{
-	System: types.IdentitySource{
-		Name:    "vsphere.local",
-		Domains: []types.Domain{{Name: "vsphere.local", Alias: "vmwarem"}},
-	},
-	LocalOS: &types.IdentitySource{
-		Name:    "localos",
-		Domains: []types.Domain{{Name: "localos", Alias: ""}},
-	},
-	NativeAD: nil,
-	LDAPS: []types.LdapIdentitySource{
-		{
-			IdentitySource: types.IdentitySource{
-				Name:    "example.com",
-				Domains: []types.Domain{{Name: "example.com", Alias: "examplex"}},
-			},
-			Type: "ActiveDirectory",
-			Details: types.LdapIdentitySourceDetails{
-				FriendlyName: "foo",
-				UserBaseDn:   "ou=People,dc=example,dc=org",
-				GroupBaseDn:  "ou=People,dc=example,dc=org",
-				PrimaryURL:   "ldap://10.168.194.120:389",
-				FailoverURL:  "",
-			},
-			AuthenticationDetails: types.AuthenticationDetails{
-				AuthenticationType: "PASSWORD",
-				Username:           "cn=admin,dc=example,dc=org",
-			},
-		},
-	},
-}
-
-type ServiceInstance struct {
-	vim.ManagedObjectReference
-}
-
-type GroupcheckServiceInstance struct {
-	vim.ManagedObjectReference
-}
-
-type SessionManager struct {
-	vim.ManagedObjectReference
-}
-
-type IdentitySourceManagementService struct {
-	vim.ManagedObjectReference
-}
-
-func New() *simulator.Registry {
-	r := simulator.NewRegistry()
-	r.Namespace = ssoadmin.Namespace
-	r.Path = ssoadmin.Path
-
-	r.Put(&ServiceInstance{
-		ManagedObjectReference: ssoadmin.ServiceInstance,
-	})
-
-	r.Put(&GroupcheckServiceInstance{
-		ManagedObjectReference: groupcheckServiceInstance,
-	})
-
-	r.Put(&SessionManager{
-		ManagedObjectReference: content.SessionManager,
-	})
-
-	r.Put(&IdentitySourceManagementService{
-		ManagedObjectReference: content.IdentitySourceManagementService,
-	})
-
-	return r
-}
-
-func (s *ServiceInstance) SsoAdminServiceInstance(ctx *simulator.Context, _ *types.SsoAdminServiceInstance) soap.HasFault {
-	return &methods.SsoAdminServiceInstanceBody{
-		Res: &types.SsoAdminServiceInstanceResponse{
-			Returnval: content,
-		},
-	}
-}
-
-func (s *GroupcheckServiceInstance) SsoGroupcheckServiceInstance(ctx *simulator.Context, _ *types.SsoGroupcheckServiceInstance) soap.HasFault {
-	return &methods.SsoGroupcheckServiceInstanceBody{
-		Res: &types.SsoGroupcheckServiceInstanceResponse{
-			Returnval: types.GroupcheckServiceContent{},
-		},
-	}
-}
-
-func (s *SessionManager) Login(ctx *simulator.Context, req *types.Login) soap.HasFault {
-	body := new(methods.LoginBody)
-
-	// TODO: check for Assertion>Subject>NameID as simulator.SessionManager.LoginByToken does
-	body.Res = new(types.LoginResponse)
-
-	return body
-}
-
-func (s *SessionManager) Logout(ctx *simulator.Context, req *types.Logout) soap.HasFault {
-	return &methods.LogoutBody{
-		Res: new(types.LogoutResponse),
-	}
-}
-
-func (s *IdentitySourceManagementService) Get(ctx *simulator.Context, _ *types.Get) soap.HasFault {
-	sources := IdentitySources
-	sources.All = nil
-	sources.All = append(sources.All, sources.System)
-
-	if sources.LocalOS != nil {
-		sources.All = append(sources.All, *sources.LocalOS)
-	}
-
-	if sources.NativeAD != nil {
-		sources.All = append(sources.All, *sources.NativeAD)
-	}
-
-	for _, ldap := range sources.LDAPS {
-		sources.All = append(sources.All, ldap.IdentitySource)
-	}
-
-	return &methods.GetBody{
-		Res: &types.GetResponse{
-			Returnval: sources,
-		},
-	}
-}
diff --git a/ssoadmin/types/types.go b/ssoadmin/types/types.go
index 49e520bc..3608dfff 100644
--- a/ssoadmin/types/types.go
+++ b/ssoadmin/types/types.go
@@ -1038,62 +1038,6 @@ type FindUsersResponse struct {
 	Returnval []AdminUser `xml:"returnval,omitempty"`
 }
 
-type Domain struct {
-	Name  string `xml:"name"`
-	Alias string `xml:"alias,omitempty"`
-}
-
-type IdentitySource struct {
-	Name    string   `xml:"name"`
-	Domains []Domain `xml:"domains"`
-}
-
-type LdapIdentitySourceDetails struct {
-	FriendlyName string `xml:"friendlyName"`
-	UserBaseDn   string `xml:"userBaseDn,omitempty"`
-	GroupBaseDn  string `xml:"groupBaseDn,omitempty"`
-	PrimaryURL   string `xml:"primaryUrl"`
-	FailoverURL  string `xml:"failoverUrl,omitempty"`
-}
-
-type AuthenticationDetails struct {
-	AuthenticationType string `xml:"authenticationType"`
-	Username           string `xml:"username"`
-}
-
-type LdapIdentitySource struct {
-	IdentitySource
-	Type                  string                    `xml:"type"`
-	Details               LdapIdentitySourceDetails `xml:"details"`
-	AuthenticationDetails AuthenticationDetails     `xml:"authenticationDetails"`
-}
-
-type IdentitySources struct {
-	All      []IdentitySource     `xml:"all"`
-	System   IdentitySource       `xml:"system"`
-	LocalOS  *IdentitySource      `xml:"localOS"`
-	NativeAD *IdentitySource      `xml:"nativeAD"`
-	LDAPS    []LdapIdentitySource `xml:"ldaps"`
-}
-
-type Get GetRequestType
-
-func init() {
-	types.Add("sso:Get", reflect.TypeOf((*Get)(nil)).Elem())
-}
-
-type GetRequestType struct {
-	This types.ManagedObjectReference `xml:"_this"`
-}
-
-func init() {
-	types.Add("sso:GetRequestType", reflect.TypeOf((*GetRequestType)(nil)).Elem())
-}
-
-type GetResponse struct {
-	Returnval IdentitySources `xml:"returnval,omitempty"`
-}
-
 type GetAllCertificates GetAllCertificatesRequestType
 
 func init() {
diff --git a/sts/client.go b/sts/client.go
index d98c5609..bcd346b7 100644
--- a/sts/client.go
+++ b/sts/client.go
@@ -71,9 +71,6 @@ func (c *Client) RoundTrip(ctx context.Context, req, res soap.HasFault) error {
 
 // TokenRequest parameters for issuing a SAML token.
 // At least one of Userinfo or Certificate must be specified.
-// When `TokenRequest.Certificate` is set, the `tls.Certificate.PrivateKey` field must be set as it is required to sign the request.
-// When the `tls.Certificate.Certificate` field is not set, the request Assertion header is set to that of the TokenRequest.Token.
-// Otherwise `tls.Certificate.Certificate` is used as the BinarySecurityToken in the request.
 type TokenRequest struct {
 	Userinfo    *url.Userinfo    // Userinfo when set issues a Bearer token
 	Certificate *tls.Certificate // Certificate when set issues a HoK token
diff --git a/sts/signer.go b/sts/signer.go
index c6bf49d9..78cf2046 100644
--- a/sts/signer.go
+++ b/sts/signer.go
@@ -128,28 +128,20 @@ func (s *Signer) Sign(env soap.Envelope) ([]byte, error) {
 			req := x.RequestSecurityToken()
 			c14n = req.C14N()
 			body = req.String()
+			id := newID()
 
-			if len(s.Certificate.Certificate) == 0 {
-				header.Assertion = s.Token
-				if err := s.setTokenReference(&info); err != nil {
-					return nil, err
-				}
-			} else {
-				id := newID()
-
-				header.BinarySecurityToken = &internal.BinarySecurityToken{
-					EncodingType: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary",
-					ValueType:    "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3",
-					ID:           id,
-					Value:        base64.StdEncoding.EncodeToString(s.Certificate.Certificate[0]),
-				}
-
-				info.SecurityTokenReference = &internal.SecurityTokenReference{
-					Reference: &internal.SecurityReference{
-						URI:       "#" + id,
-						ValueType: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3",
-					},
-				}
+			info.SecurityTokenReference = &internal.SecurityTokenReference{
+				Reference: &internal.SecurityReference{
+					URI:       "#" + id,
+					ValueType: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3",
+				},
+			}
+
+			header.BinarySecurityToken = &internal.BinarySecurityToken{
+				EncodingType: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary",
+				ValueType:    "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3",
+				ID:           id,
+				Value:        base64.StdEncoding.EncodeToString(s.Certificate.Certificate[0]),
 			}
 		}
 		// When requesting HoK token for interactive user, request will have both priv. key and username/password.
diff --git a/vapi/namespace/namespace.go b/vapi/namespace/namespace.go
index 3e1c21d5..c22bfddc 100644
--- a/vapi/namespace/namespace.go
+++ b/vapi/namespace/namespace.go
@@ -188,7 +188,7 @@ type EdgeClusterCompatibilitySummary struct {
 func (c *Manager) ListCompatibleEdgeClusters(ctx context.Context, clusterId string, switchId string) (result []EdgeClusterCompatibilitySummary, err error) {
 	listUrl := c.Resource(internal.NamespaceEdgeClusterCompatibility).
 		WithParam("cluster", clusterId).
-		WithParam("distributed_switch", switchId).
-		WithParam("compatible", "true")
+		WithParam("compatible", "true").
+		WithPathEncodedParam("distributed_switch", switchId)
 	return result, c.Do(ctx, listUrl.Request(http.MethodGet), &result)
 }
diff --git a/vapi/namespace/simulator/simulator.go b/vapi/namespace/simulator/simulator.go
index 285a8696..fc773928 100644
--- a/vapi/namespace/simulator/simulator.go
+++ b/vapi/namespace/simulator/simulator.go
@@ -237,6 +237,6 @@ func (h *Handler) getService(w http.ResponseWriter, r *http.Request) {
 				return
 			}
 		}
-		w.WriteHeader(http.StatusNotFound)
+		vapi.Status(w, http.StatusNotFound)
 	}
 }
diff --git a/vapi/rest/resource.go b/vapi/rest/resource.go
index 3004abb8..e6e62749 100644
--- a/vapi/rest/resource.go
+++ b/vapi/rest/resource.go
@@ -51,12 +51,36 @@ func (r *Resource) WithAction(action string) *Resource {
 // WithParam adds one parameter on the URL.RawQuery
 func (r *Resource) WithParam(name string, value string) *Resource {
 	// ParseQuery handles empty case, and we control access to query string so shouldn't encounter an error case
-	params, _ := url.ParseQuery(r.u.RawQuery)
+	params, err := url.ParseQuery(r.u.RawQuery)
+	if err != nil {
+		panic(err)
+	}
 	params[name] = append(params[name], value)
 	r.u.RawQuery = params.Encode()
 	return r
 }
 
+// WithPathEncodedParam appends a parameter on the URL.RawQuery,
+// For special cases where URL Path-style encoding is needed
+func (r *Resource) WithPathEncodedParam(name string, value string) *Resource {
+	t := &url.URL{Path: value}
+	encodedValue := t.String()
+	t = &url.URL{Path: name}
+	encodedName := t.String()
+	// ParseQuery handles empty case, and we control access to query string so shouldn't encounter an error case
+	params, err := url.ParseQuery(r.u.RawQuery)
+	if err != nil {
+		panic(err)
+	}
+	// Values.Encode() doesn't escape exactly how we want, so we need to build the query string ourselves
+	if len(params) >= 1 {
+		r.u.RawQuery = r.u.RawQuery + "&" + encodedName + "=" + encodedValue
+	} else {
+		r.u.RawQuery = r.u.RawQuery + encodedName + "=" + encodedValue
+	}
+	return r
+}
+
 // Request returns a new http.Request for the given method.
 // An optional body can be provided for POST and PATCH methods.
 func (r *Resource) Request(method string, body ...interface{}) *http.Request {
diff --git a/vapi/simulator/simulator.go b/vapi/simulator/simulator.go
index 92160a97..fcc7461b 100644
--- a/vapi/simulator/simulator.go
+++ b/vapi/simulator/simulator.go
@@ -295,10 +295,10 @@ func (s *handler) DetachTag(id vim.ManagedObjectReference, tag vim.VslmTagEntry)
 	return nil
 }
 
-// StatusOK responds with http.StatusOK and encodes val, if specified, to JSON
+// Status responds with custom HTTP status and json encoded val if given.
 // For use with "/api" endpoints.
-func StatusOK(w http.ResponseWriter, val ...interface{}) {
-	w.WriteHeader(http.StatusOK)
+func Status(w http.ResponseWriter, httpStatus int, val ...interface{}) {
+	w.WriteHeader(httpStatus)
 	if len(val) == 0 {
 		return
 	}
@@ -310,7 +310,13 @@ func StatusOK(w http.ResponseWriter, val ...interface{}) {
 	}
 }
 
-// OK responds with http.StatusOK and encodes val, if specified, to JSON
+// StatusOK responds with http.StatusOK and json encoded val if given.
+// For use with "/api" endpoints.
+func StatusOK(w http.ResponseWriter, val ...interface{}) {
+	Status(w, http.StatusOK, val...)
+}
+
+// OK responds with http.StatusOK and json encoded val if given.
 // For use with "/rest" endpoints where the response is a "value" wrapped structure.
 func OK(w http.ResponseWriter, val ...interface{}) {
 	if len(val) == 0 {
diff --git a/vcsim/main.go b/vcsim/main.go
index 7876ec61..fcc4c288 100644
--- a/vcsim/main.go
+++ b/vcsim/main.go
@@ -44,7 +44,6 @@ import (
 	_ "github.com/vmware/govmomi/eam/simulator"
 	_ "github.com/vmware/govmomi/lookup/simulator"
 	_ "github.com/vmware/govmomi/pbm/simulator"
-	_ "github.com/vmware/govmomi/ssoadmin/simulator"
 	_ "github.com/vmware/govmomi/sts/simulator"
 	_ "github.com/vmware/govmomi/vapi/cluster/simulator"
 	_ "github.com/vmware/govmomi/vapi/namespace/simulator"


Test patch:
diff --git a/govc/test/import.bats b/govc/test/import.bats
index 7550fad8..3d714614 100755
--- a/govc/test/import.bats
+++ b/govc/test/import.bats
@@ -22,9 +22,6 @@ load test_helper
   run govc import.spec "https://$(govc env GOVC_URL)/folder/$TTYLINUX_NAME.ovf"
   assert_success
 
-  run govc import.spec "https://$(govc env GOVC_URL)/folder/$TTYLINUX_NAME.ova"
-  assert_success
-
   proto=$(jq -r .IPProtocol <<<"$output")
   assert_equal IPv4 "$proto"
 
diff --git a/govc/test/sso.bats b/govc/test/sso.bats
index d03c34e9..ff67ddb1 100755
--- a/govc/test/sso.bats
+++ b/govc/test/sso.bats
@@ -45,18 +45,4 @@ load test_helper
   govc sso.service.ls -p com.vmware.cis -t cs.identity -P wsTrust -T com.vmware.cis.cs.identity.sso -l | grep wsTrust
   govc sso.service.ls -P vmomi | grep vcenterserver | grep -v https:
   govc sso.service.ls -P vmomi -l | grep https:
-}
-
-@test "sso.idp.ls" {
-  vcsim_env
-
-  run govc sso.idp.ls -json
-  assert_success
-
-  run govc sso.idp.ls
-  assert_success
-  [ ${#lines[@]} -eq 4 ]
-  assert_matches "System Domain"
-  assert_matches "Local OS"
-  assert_matches "ActiveDirectory"
-}
+}
\ No newline at end of file
diff --git a/simulator/dvs_test.go b/simulator/dvs_test.go
index 138f578a..6fcb2716 100644
--- a/simulator/dvs_test.go
+++ b/simulator/dvs_test.go
@@ -124,9 +124,7 @@ func TestDVS(t *testing.T) {
 		} else {
 			switch test.op {
 			case types.ConfigSpecOperationAdd:
-				dtask, err = dswitch.AddPortgroup(ctx, []types.DVPortgroupConfigSpec{
-					{Name: test.pg, NumPorts: 1},
-				})
+				dtask, err = dswitch.AddPortgroup(ctx, []types.DVPortgroupConfigSpec{{Name: test.pg}})
 			}
 		}
 
diff --git a/simulator/folder_test.go b/simulator/folder_test.go
index 9fb3e0b5..a84c7c64 100644
--- a/simulator/folder_test.go
+++ b/simulator/folder_test.go
@@ -214,50 +214,6 @@ func TestFolderVC(t *testing.T) {
 	}
 }
 
-func TestFolderSpecialCharaters(t *testing.T) {
-	content := vpx.ServiceContent
-	s := New(NewServiceInstance(SpoofContext(), content, vpx.RootFolder))
-
-	ts := s.NewServer()
-	defer ts.Close()
-
-	ctx := context.Background()
-	c, err := govmomi.NewClient(ctx, ts.URL, true)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	f := object.NewRootFolder(c.Client)
-
-	tests := []struct {
-		name     string
-		expected string
-	}{
-		{`/`, `%2f`},
-		{`\`, `%5c`},
-		{`%`, `%25`},
-		// multiple special characters
-		{`%%`, `%25%25`},
-	}
-
-	for _, test := range tests {
-		ff, err := f.CreateFolder(ctx, test.name)
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		o := Map.Get(ff.Reference())
-		if o == nil {
-			t.Fatalf("failed to find %#v", ff)
-		}
-
-		e := o.(mo.Entity).Entity()
-		if e.Name != test.expected {
-			t.Errorf("expected %s, got %s", test.expected, e.Name)
-		}
-	}
-}
-
 func TestFolderFaults(t *testing.T) {
 	f := Folder{}
 	f.ChildType = []string{"VirtualMachine"}
diff --git a/sts/client_test.go b/sts/client_test.go
index 8196e50c..03014039 100644
--- a/sts/client_test.go
+++ b/sts/client_test.go
@@ -122,7 +122,6 @@ func TestIssueHOK(t *testing.T) {
 	if err != nil {
 		log.Fatal(err)
 	}
-	_ = c.UseServiceVersion()
 
 	if !c.IsVC() {
 		t.SkipNow()
@@ -162,75 +161,6 @@ func TestIssueHOK(t *testing.T) {
 	log.Printf("current time=%s", now)
 }
 
-func TestIssueTokenByToken(t *testing.T) {
-	ctx := context.Background()
-	url := os.Getenv("GOVC_TEST_URL")
-	if url == "" {
-		t.SkipNow()
-	}
-
-	u, err := soap.ParseURL(url)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	vc1, err := vim25.NewClient(ctx, soap.NewClient(u, true))
-	if err != nil {
-		log.Fatal(err)
-	}
-	_ = vc1.UseServiceVersion()
-
-	if !vc1.IsVC() {
-		t.SkipNow()
-	}
-
-	vc2, err := vim25.NewClient(ctx, soap.NewClient(u, true))
-	if err != nil {
-		log.Fatal(err)
-	}
-	_ = vc2.UseServiceVersion()
-
-	sts1, err := NewClient(ctx, vc1)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if err = solutionUserCreate(ctx, u.User, sts1, vc1); err != nil {
-		t.Fatal(err)
-	}
-
-	sts2, err := NewClient(ctx, vc2)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	req1 := TokenRequest{
-		Certificate: solutionUserCert(),
-		Delegatable: true,
-	}
-
-	signer1, err := sts1.Issue(ctx, req1)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	req2 := signer1.NewRequest()
-	// use Assertion header instead of BinarySecurityToken
-	req2.Certificate = &tls.Certificate{PrivateKey: req1.Certificate.PrivateKey}
-
-	signer2, err := sts2.Issue(ctx, req2)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	header := soap.Header{Security: signer2}
-
-	err = session.NewManager(vc2).LoginByToken(vc2.WithHeader(ctx, header))
-	if err != nil {
-		t.Fatal(err)
-	}
-}
-
 func TestIssueBearer(t *testing.T) {
 	ctx := context.Background()
 	url := os.Getenv("GOVC_TEST_URL")
@@ -247,7 +177,6 @@ func TestIssueBearer(t *testing.T) {
 	if err != nil {
 		log.Fatal(err)
 	}
-	_ = c.UseServiceVersion()
 
 	if !c.IsVC() {
 		t.SkipNow()
@@ -304,7 +233,6 @@ func TestIssueActAs(t *testing.T) {
 	if err != nil {
 		log.Fatal(err)
 	}
-	_ = c.UseServiceVersion()
 
 	if !c.IsVC() {
 		t.SkipNow()
diff --git a/vapi/rest/resource_test.go b/vapi/rest/resource_test.go
index 31778321..5ee40d66 100644
--- a/vapi/rest/resource_test.go
+++ b/vapi/rest/resource_test.go
@@ -51,3 +51,34 @@ func TestResource_WithParam(t *testing.T) {
 		}
 	})
 }
+
+func TestResource_WithPathEncodedParam(t *testing.T) {
+	simulator.Test(func(ctx context.Context, vc *vim25.Client) {
+		c := rest.NewClient(vc)
+
+		// path is correctly formatted when Path-Encoded param is first
+		url := c.Resource("api/some/resource").
+			WithPathEncodedParam("key1", "value 1")
+		expectedPath := "api/some/resource?key1=value%201"
+		if !strings.Contains(url.String(), expectedPath) {
+			t.Errorf("First path-encoded param incorrectly added to resource, URL %q, expected path %q", url.String(), expectedPath)
+		}
+
+		// path is correctly formatted when Path-Encoded param is last
+		url = c.Resource("api/some/resource").
+			WithParam("key1", "value 1").
+			WithPathEncodedParam("key2", "value 2")
+		expectedPath = "api/some/resource?key1=value+1&key2=value%202"
+		if !strings.Contains(url.String(), expectedPath) {
+			t.Errorf("Last path-encoded param incorrectly added to resource, URL %q, expected path %q", url.String(), expectedPath)
+		}
+
+		// if WithParam is used again, it will re-encode the Path-Encoded value
+		url = url.WithParam("key3", "value 3")
+		expectedPath = "api/some/resource?key1=value+1&key2=value+2&key3=value+3"
+		if !strings.Contains(url.String(), expectedPath) {
+			t.Errorf("Middle path-encoded param not endcoded as expected, URL %q, expected path %q", url.String(), expectedPath)
+		}
+
+	})
+}