// Copyright IBM Corp. 2015, 2025
// SPDX-License-Identifier: MPL-2.0

package csi

import (
	"context"
	"errors"
	"fmt"
	"path/filepath"
	"testing"
	"time"

	csipbv1 "github.com/container-storage-interface/spec/lib/go/csi"
	"github.com/golang/protobuf/ptypes/wrappers"
	"github.com/shoenig/test/must"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/nomad/structs"
	fake "github.com/hashicorp/nomad/plugins/csi/testing"
)

func newTestClient(t *testing.T) (*fake.IdentityClient, *fake.ControllerClient, *fake.NodeClient, CSIPlugin) {
	ic := fake.NewIdentityClient()
	cc := fake.NewControllerClient()
	nc := fake.NewNodeClient()

	// we've set this as non-blocking so it won't connect to the
	// socket unless a RPC is invoked
	conn, err := grpc.DialContext(context.Background(),
		filepath.Join(t.TempDir(), "csi.sock"), grpc.WithInsecure())
	if err != nil {
		t.Errorf("failed: %v", err)
	}

	client := &client{
		conn:             conn,
		identityClient:   ic,
		controllerClient: cc,
		nodeClient:       nc,
	}
	t.Cleanup(func() {
		_ = client.Close()
	})

	return ic, cc, nc, client
}

func TestClient_RPC_PluginProbe(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name             string
		ResponseErr      error
		ProbeResponse    *csipbv1.ProbeResponse
		ExpectedResponse bool
		ExpectedErr      error
	}{
		{
			Name:        "handles underlying grpc errors",
			ResponseErr: fmt.Errorf("some grpc error"),
			ExpectedErr: fmt.Errorf("some grpc error"),
		},
		{
			Name: "returns false for ready when the provider returns false",
			ProbeResponse: &csipbv1.ProbeResponse{
				Ready: &wrappers.BoolValue{Value: false},
			},
			ExpectedResponse: false,
		},
		{
			Name: "returns true for ready when the provider returns true",
			ProbeResponse: &csipbv1.ProbeResponse{
				Ready: &wrappers.BoolValue{Value: true},
			},
			ExpectedResponse: true,
		},
		{
			/* When a SP does not return a ready value, a CO MAY treat this as ready.
			   We do so because example plugins rely on this behaviour. We may
				 re-evaluate this decision in the future. */
			Name: "returns true for ready when the provider returns a nil wrapper",
			ProbeResponse: &csipbv1.ProbeResponse{
				Ready: nil,
			},
			ExpectedResponse: true,
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			ic, _, _, client := newTestClient(t)
			defer client.Close()

			ic.NextErr = tc.ResponseErr
			ic.NextPluginProbe = tc.ProbeResponse

			resp, err := client.PluginProbe(context.TODO())
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
			}

			must.Eq(t, tc.ExpectedResponse, resp)
		})
	}

}

func TestClient_RPC_PluginInfo(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name                    string
		ResponseErr             error
		InfoResponse            *csipbv1.GetPluginInfoResponse
		ExpectedResponseName    string
		ExpectedResponseVersion string
		ExpectedErr             error
	}{
		{
			Name:        "handles underlying grpc errors",
			ResponseErr: fmt.Errorf("some grpc error"),
			ExpectedErr: fmt.Errorf("some grpc error"),
		},
		{
			Name: "returns an error if we receive an empty `name`",
			InfoResponse: &csipbv1.GetPluginInfoResponse{
				Name:          "",
				VendorVersion: "",
			},
			ExpectedErr: fmt.Errorf("PluginGetInfo: plugin returned empty name field"),
		},
		{
			Name: "returns the name when successfully retrieved and not empty",
			InfoResponse: &csipbv1.GetPluginInfoResponse{
				Name:          "com.hashicorp.storage",
				VendorVersion: "1.0.1",
			},
			ExpectedResponseName:    "com.hashicorp.storage",
			ExpectedResponseVersion: "1.0.1",
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			ic, _, _, client := newTestClient(t)
			defer client.Close()

			ic.NextErr = tc.ResponseErr
			ic.NextPluginInfo = tc.InfoResponse

			name, version, err := client.PluginGetInfo(context.TODO())
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
			}

			must.Eq(t, tc.ExpectedResponseName, name)
			must.Eq(t, tc.ExpectedResponseVersion, version)
		})
	}

}

func TestClient_RPC_PluginGetCapabilities(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name             string
		ResponseErr      error
		Response         *csipbv1.GetPluginCapabilitiesResponse
		ExpectedResponse *PluginCapabilitySet
		ExpectedErr      error
	}{
		{
			Name:        "handles underlying grpc errors",
			ResponseErr: fmt.Errorf("some grpc error"),
			ExpectedErr: fmt.Errorf("some grpc error"),
		},
		{
			Name: "HasControllerService is true when it's part of the response",
			Response: &csipbv1.GetPluginCapabilitiesResponse{
				Capabilities: []*csipbv1.PluginCapability{
					{
						Type: &csipbv1.PluginCapability_Service_{
							Service: &csipbv1.PluginCapability_Service{
								Type: csipbv1.PluginCapability_Service_CONTROLLER_SERVICE,
							},
						},
					},
				},
			},
			ExpectedResponse: &PluginCapabilitySet{hasControllerService: true},
		},
		{
			Name: "HasTopologies is true when it's part of the response",
			Response: &csipbv1.GetPluginCapabilitiesResponse{
				Capabilities: []*csipbv1.PluginCapability{
					{
						Type: &csipbv1.PluginCapability_Service_{
							Service: &csipbv1.PluginCapability_Service{
								Type: csipbv1.PluginCapability_Service_VOLUME_ACCESSIBILITY_CONSTRAINTS,
							},
						},
					},
				},
			},
			ExpectedResponse: &PluginCapabilitySet{hasTopologies: true},
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			ic, _, _, client := newTestClient(t)
			defer client.Close()

			ic.NextErr = tc.ResponseErr
			ic.NextPluginCapabilities = tc.Response

			resp, err := client.PluginGetCapabilities(context.TODO())
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
			}

			must.Eq(t, tc.ExpectedResponse, resp)
		})
	}
}

func TestClient_RPC_ControllerGetCapabilities(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name             string
		ResponseErr      error
		Response         *csipbv1.ControllerGetCapabilitiesResponse
		ExpectedResponse *ControllerCapabilitySet
		ExpectedErr      error
	}{
		{
			Name:        "handles underlying grpc errors",
			ResponseErr: fmt.Errorf("some grpc error"),
			ExpectedErr: fmt.Errorf("some grpc error"),
		},
		{
			Name: "ignores unknown capabilities",
			Response: &csipbv1.ControllerGetCapabilitiesResponse{
				Capabilities: []*csipbv1.ControllerServiceCapability{
					{
						Type: &csipbv1.ControllerServiceCapability_Rpc{
							Rpc: &csipbv1.ControllerServiceCapability_RPC{
								Type: csipbv1.ControllerServiceCapability_RPC_UNKNOWN,
							},
						},
					},
				},
			},
			ExpectedResponse: &ControllerCapabilitySet{},
		},
		{
			Name: "detects list volumes capabilities",
			Response: &csipbv1.ControllerGetCapabilitiesResponse{
				Capabilities: []*csipbv1.ControllerServiceCapability{
					{
						Type: &csipbv1.ControllerServiceCapability_Rpc{
							Rpc: &csipbv1.ControllerServiceCapability_RPC{
								Type: csipbv1.ControllerServiceCapability_RPC_LIST_VOLUMES,
							},
						},
					},
					{
						Type: &csipbv1.ControllerServiceCapability_Rpc{
							Rpc: &csipbv1.ControllerServiceCapability_RPC{
								Type: csipbv1.ControllerServiceCapability_RPC_LIST_VOLUMES_PUBLISHED_NODES,
							},
						},
					},
				},
			},
			ExpectedResponse: &ControllerCapabilitySet{
				HasListVolumes:               true,
				HasListVolumesPublishedNodes: true,
			},
		},
		{
			Name: "detects publish capabilities",
			Response: &csipbv1.ControllerGetCapabilitiesResponse{
				Capabilities: []*csipbv1.ControllerServiceCapability{
					{
						Type: &csipbv1.ControllerServiceCapability_Rpc{
							Rpc: &csipbv1.ControllerServiceCapability_RPC{
								Type: csipbv1.ControllerServiceCapability_RPC_PUBLISH_READONLY,
							},
						},
					},
					{
						Type: &csipbv1.ControllerServiceCapability_Rpc{
							Rpc: &csipbv1.ControllerServiceCapability_RPC{
								Type: csipbv1.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME,
							},
						},
					},
				},
			},
			ExpectedResponse: &ControllerCapabilitySet{
				HasPublishUnpublishVolume: true,
				HasPublishReadonly:        true,
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, cc, _, client := newTestClient(t)
			defer client.Close()

			cc.NextErr = tc.ResponseErr
			cc.NextCapabilitiesResponse = tc.Response

			resp, err := client.ControllerGetCapabilities(context.TODO())
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
			}

			must.Eq(t, tc.ExpectedResponse, resp)
		})
	}
}

func TestClient_RPC_NodeGetCapabilities(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name             string
		ResponseErr      error
		Response         *csipbv1.NodeGetCapabilitiesResponse
		ExpectedResponse *NodeCapabilitySet
		ExpectedErr      error
	}{
		{
			Name:        "handles underlying grpc errors",
			ResponseErr: fmt.Errorf("some grpc error"),
			ExpectedErr: fmt.Errorf("some grpc error"),
		},
		{
			Name: "detects multiple capabilities",
			Response: &csipbv1.NodeGetCapabilitiesResponse{
				Capabilities: []*csipbv1.NodeServiceCapability{
					{
						Type: &csipbv1.NodeServiceCapability_Rpc{
							Rpc: &csipbv1.NodeServiceCapability_RPC{
								Type: csipbv1.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME,
							},
						},
					},
					{
						Type: &csipbv1.NodeServiceCapability_Rpc{
							Rpc: &csipbv1.NodeServiceCapability_RPC{
								Type: csipbv1.NodeServiceCapability_RPC_EXPAND_VOLUME,
							},
						},
					},
				},
			},
			ExpectedResponse: &NodeCapabilitySet{
				HasStageUnstageVolume: true,
				HasExpandVolume:       true,
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, _, nc, client := newTestClient(t)
			defer client.Close()

			nc.NextErr = tc.ResponseErr
			nc.NextCapabilitiesResponse = tc.Response

			resp, err := client.NodeGetCapabilities(context.TODO())
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
			}

			must.Eq(t, tc.ExpectedResponse, resp)
		})
	}
}

func TestClient_RPC_ControllerPublishVolume(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name             string
		Request          *ControllerPublishVolumeRequest
		ResponseErr      error
		Response         *csipbv1.ControllerPublishVolumeResponse
		ExpectedResponse *ControllerPublishVolumeResponse
		ExpectedErr      error
	}{
		{
			Name:        "handles underlying grpc errors",
			Request:     &ControllerPublishVolumeRequest{ExternalID: "vol", NodeID: "node"},
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},
		{
			Name:        "handles missing NodeID",
			Request:     &ControllerPublishVolumeRequest{ExternalID: "vol"},
			Response:    &csipbv1.ControllerPublishVolumeResponse{},
			ExpectedErr: fmt.Errorf("missing NodeID"),
		},

		{
			Name: "handles PublishContext == nil",
			Request: &ControllerPublishVolumeRequest{
				ExternalID: "vol", NodeID: "node"},
			Response:         &csipbv1.ControllerPublishVolumeResponse{},
			ExpectedResponse: &ControllerPublishVolumeResponse{},
		},
		{
			Name:    "handles PublishContext != nil",
			Request: &ControllerPublishVolumeRequest{ExternalID: "vol", NodeID: "node"},
			Response: &csipbv1.ControllerPublishVolumeResponse{
				PublishContext: map[string]string{
					"com.hashicorp/nomad-node-id": "foobar",
					"com.plugin/device":           "/dev/sdc1",
				},
			},
			ExpectedResponse: &ControllerPublishVolumeResponse{
				PublishContext: map[string]string{
					"com.hashicorp/nomad-node-id": "foobar",
					"com.plugin/device":           "/dev/sdc1",
				},
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, cc, _, client := newTestClient(t)
			defer client.Close()

			cc.NextErr = tc.ResponseErr
			cc.NextPublishVolumeResponse = tc.Response

			resp, err := client.ControllerPublishVolume(context.TODO(), tc.Request)
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
			}

			must.Eq(t, tc.ExpectedResponse, resp)
		})
	}
}

func TestClient_RPC_ControllerUnpublishVolume(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name             string
		Request          *ControllerUnpublishVolumeRequest
		ResponseErr      error
		Response         *csipbv1.ControllerUnpublishVolumeResponse
		ExpectedResponse *ControllerUnpublishVolumeResponse
		ExpectedErr      error
	}{
		{
			Name:        "handles underlying grpc errors",
			Request:     &ControllerUnpublishVolumeRequest{ExternalID: "vol", NodeID: "node"},
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},
		{
			Name:             "handles missing NodeID",
			Request:          &ControllerUnpublishVolumeRequest{ExternalID: "vol"},
			ExpectedErr:      fmt.Errorf("missing NodeID"),
			ExpectedResponse: nil,
		},
		{
			Name:             "handles successful response",
			Request:          &ControllerUnpublishVolumeRequest{ExternalID: "vol", NodeID: "node"},
			ExpectedResponse: &ControllerUnpublishVolumeResponse{},
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, cc, _, client := newTestClient(t)
			defer client.Close()

			cc.NextErr = tc.ResponseErr
			cc.NextUnpublishVolumeResponse = tc.Response

			resp, err := client.ControllerUnpublishVolume(context.TODO(), tc.Request)
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
			}

			must.Eq(t, tc.ExpectedResponse, resp)
		})
	}
}

func TestClient_RPC_ControllerValidateVolume(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name        string
		AccessType  VolumeAccessType
		AccessMode  VolumeAccessMode
		ResponseErr error
		Response    *csipbv1.ValidateVolumeCapabilitiesResponse
		ExpectedErr error
	}{
		{
			Name:        "handles underlying grpc errors",
			AccessType:  VolumeAccessTypeMount,
			AccessMode:  VolumeAccessModeMultiNodeMultiWriter,
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},
		{
			Name:        "handles success empty capabilities",
			AccessType:  VolumeAccessTypeMount,
			AccessMode:  VolumeAccessModeMultiNodeMultiWriter,
			Response:    &csipbv1.ValidateVolumeCapabilitiesResponse{},
			ResponseErr: nil,
			ExpectedErr: nil,
		},
		{
			Name:       "handles success exact match MountVolume",
			AccessType: VolumeAccessTypeMount,
			AccessMode: VolumeAccessModeMultiNodeMultiWriter,
			Response: &csipbv1.ValidateVolumeCapabilitiesResponse{
				Confirmed: &csipbv1.ValidateVolumeCapabilitiesResponse_Confirmed{
					VolumeContext: map[string]string{},
					VolumeCapabilities: []*csipbv1.VolumeCapability{
						{
							AccessType: &csipbv1.VolumeCapability_Mount{
								Mount: &csipbv1.VolumeCapability_MountVolume{
									FsType:     "ext4",
									MountFlags: []string{"errors=remount-ro", "noatime"},
								},
							},
							AccessMode: &csipbv1.VolumeCapability_AccessMode{
								Mode: csipbv1.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,
							},
						},
					},
				},
			},
			ResponseErr: nil,
			ExpectedErr: nil,
		},

		{
			Name:       "handles success exact match BlockVolume",
			AccessType: VolumeAccessTypeBlock,
			AccessMode: VolumeAccessModeMultiNodeMultiWriter,
			Response: &csipbv1.ValidateVolumeCapabilitiesResponse{
				Confirmed: &csipbv1.ValidateVolumeCapabilitiesResponse_Confirmed{
					VolumeCapabilities: []*csipbv1.VolumeCapability{
						{
							AccessType: &csipbv1.VolumeCapability_Block{
								Block: &csipbv1.VolumeCapability_BlockVolume{},
							},

							AccessMode: &csipbv1.VolumeCapability_AccessMode{
								Mode: csipbv1.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,
							},
						},
					},
				},
			},
			ResponseErr: nil,
			ExpectedErr: nil,
		},

		{
			Name:       "handles failure AccessMode mismatch",
			AccessMode: VolumeAccessModeMultiNodeMultiWriter,
			Response: &csipbv1.ValidateVolumeCapabilitiesResponse{
				Confirmed: &csipbv1.ValidateVolumeCapabilitiesResponse_Confirmed{
					VolumeContext: map[string]string{},
					VolumeCapabilities: []*csipbv1.VolumeCapability{
						{
							AccessType: &csipbv1.VolumeCapability_Block{
								Block: &csipbv1.VolumeCapability_BlockVolume{},
							},
							AccessMode: &csipbv1.VolumeCapability_AccessMode{
								Mode: csipbv1.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
							},
						},
					},
				},
			},
			ResponseErr: nil,
			// this is a multierror
			ExpectedErr: fmt.Errorf("volume capability validation failed: 1 error occurred:\n\t* requested access mode MULTI_NODE_MULTI_WRITER, got SINGLE_NODE_WRITER\n\n"),
		},

		{
			Name:       "handles failure MountFlags mismatch",
			AccessType: VolumeAccessTypeMount,
			AccessMode: VolumeAccessModeMultiNodeMultiWriter,
			Response: &csipbv1.ValidateVolumeCapabilitiesResponse{
				Confirmed: &csipbv1.ValidateVolumeCapabilitiesResponse_Confirmed{
					VolumeContext: map[string]string{},
					VolumeCapabilities: []*csipbv1.VolumeCapability{
						{
							AccessType: &csipbv1.VolumeCapability_Mount{
								Mount: &csipbv1.VolumeCapability_MountVolume{
									FsType:     "ext4",
									MountFlags: []string{},
								},
							},
							AccessMode: &csipbv1.VolumeCapability_AccessMode{
								Mode: csipbv1.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,
							},
						},
					},
				},
			},
			ResponseErr: nil,
			// this is a multierror
			ExpectedErr: fmt.Errorf("volume capability validation failed: 1 error occurred:\n\t* requested mount flags did not match available capabilities\n\n"),
		},

		{
			Name:       "handles failure MountFlags with Block",
			AccessType: VolumeAccessTypeBlock,
			AccessMode: VolumeAccessModeMultiNodeMultiWriter,
			Response: &csipbv1.ValidateVolumeCapabilitiesResponse{
				Confirmed: &csipbv1.ValidateVolumeCapabilitiesResponse_Confirmed{
					VolumeContext: map[string]string{},
					VolumeCapabilities: []*csipbv1.VolumeCapability{
						{
							AccessType: &csipbv1.VolumeCapability_Mount{
								Mount: &csipbv1.VolumeCapability_MountVolume{
									FsType:     "ext4",
									MountFlags: []string{},
								},
							},
							AccessMode: &csipbv1.VolumeCapability_AccessMode{
								Mode: csipbv1.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,
							},
						},
					},
				},
			},
			ResponseErr: nil,
			// this is a multierror
			ExpectedErr: fmt.Errorf("volume capability validation failed: 1 error occurred:\n\t* 'file-system' access type was not requested but was validated by the controller\n\n"),
		},

		{
			Name:       "handles success incomplete no AccessType",
			AccessType: VolumeAccessTypeMount,
			AccessMode: VolumeAccessModeMultiNodeMultiWriter,
			Response: &csipbv1.ValidateVolumeCapabilitiesResponse{
				Confirmed: &csipbv1.ValidateVolumeCapabilitiesResponse_Confirmed{
					VolumeCapabilities: []*csipbv1.VolumeCapability{
						{
							AccessMode: &csipbv1.VolumeCapability_AccessMode{
								Mode: csipbv1.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,
							},
						},
					},
				},
			},
			ResponseErr: nil,
			ExpectedErr: nil,
		},

		{
			Name:       "handles success incomplete no AccessMode",
			AccessType: VolumeAccessTypeBlock,
			AccessMode: VolumeAccessModeMultiNodeMultiWriter,
			Response: &csipbv1.ValidateVolumeCapabilitiesResponse{
				Confirmed: &csipbv1.ValidateVolumeCapabilitiesResponse_Confirmed{
					VolumeCapabilities: []*csipbv1.VolumeCapability{
						{
							AccessType: &csipbv1.VolumeCapability_Block{
								Block: &csipbv1.VolumeCapability_BlockVolume{},
							},
						},
					},
				},
			},
			ResponseErr: nil,
			ExpectedErr: nil,
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, cc, _, client := newTestClient(t)
			defer client.Close()

			requestedCaps := []*VolumeCapability{{
				AccessType: tc.AccessType,
				AccessMode: tc.AccessMode,
				MountVolume: &structs.CSIMountOptions{ // should be ignored
					FSType:     "ext4",
					MountFlags: []string{"noatime", "errors=remount-ro"},
				},
			}}
			req := &ControllerValidateVolumeRequest{
				ExternalID:   "volumeID",
				Secrets:      structs.CSISecrets{},
				Capabilities: requestedCaps,
				Parameters:   map[string]string{},
				Context:      map[string]string{},
			}

			cc.NextValidateVolumeCapabilitiesResponse = tc.Response
			cc.NextErr = tc.ResponseErr

			err := client.ControllerValidateCapabilities(context.TODO(), req)
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
			} else {
				must.NoError(t, err, must.Sprint("name", tc.Name))
			}
		})
	}
}

func TestClient_RPC_ControllerCreateVolume(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name          string
		CapacityRange *CapacityRange
		ContentSource *VolumeContentSource
		ResponseErr   error
		Response      *csipbv1.CreateVolumeResponse
		ExpectedErr   error
	}{
		{
			Name:        "handles underlying grpc errors",
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},

		{
			Name: "handles error invalid capacity range",
			CapacityRange: &CapacityRange{
				RequiredBytes: 1000,
				LimitBytes:    500,
			},
			ExpectedErr: errors.New("LimitBytes cannot be less than RequiredBytes"),
		},

		{
			Name: "handles error invalid content source",
			ContentSource: &VolumeContentSource{
				SnapshotID: "snap-12345",
				CloneID:    "vol-12345",
			},
			ExpectedErr: errors.New(
				"one of SnapshotID or CloneID must be set if ContentSource is set"),
		},

		{
			Name:     "handles success missing source and range",
			Response: &csipbv1.CreateVolumeResponse{},
		},

		{
			Name: "handles success with capacity range, source, and topology",
			CapacityRange: &CapacityRange{
				RequiredBytes: 500,
				LimitBytes:    1000,
			},
			ContentSource: &VolumeContentSource{
				SnapshotID: "snap-12345",
			},
			Response: &csipbv1.CreateVolumeResponse{
				Volume: &csipbv1.Volume{
					CapacityBytes: 1000,
					ContentSource: &csipbv1.VolumeContentSource{
						Type: &csipbv1.VolumeContentSource_Snapshot{
							Snapshot: &csipbv1.VolumeContentSource_SnapshotSource{
								SnapshotId: "snap-12345",
							},
						},
					},
					AccessibleTopology: []*csipbv1.Topology{
						{Segments: map[string]string{"rack": "R1"}},
					},
				},
			},
		},
	}
	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, cc, _, client := newTestClient(t)
			defer client.Close()

			req := &ControllerCreateVolumeRequest{
				Name:          "vol-123456",
				CapacityRange: tc.CapacityRange,
				VolumeCapabilities: []*VolumeCapability{
					{
						AccessType: VolumeAccessTypeMount,
						AccessMode: VolumeAccessModeMultiNodeMultiWriter,
					},
				},
				Parameters:    map[string]string{},
				Secrets:       structs.CSISecrets{},
				ContentSource: tc.ContentSource,
				AccessibilityRequirements: &TopologyRequirement{
					Requisite: []*Topology{
						{
							Segments: map[string]string{"rack": "R1"},
						},
						{
							Segments: map[string]string{"rack": "R2"},
						},
					},
				},
			}

			cc.NextCreateVolumeResponse = tc.Response
			cc.NextErr = tc.ResponseErr

			resp, err := client.ControllerCreateVolume(context.TODO(), req)
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
				return
			}
			must.NoError(t, err, must.Sprint("name", tc.Name))
			if tc.Response == nil {
				must.Nil(t, resp)
				return
			}
			if tc.CapacityRange != nil {
				must.Greater(t, 0, resp.Volume.CapacityBytes)
			}
			if tc.ContentSource != nil {
				must.Eq(t, tc.ContentSource.CloneID, resp.Volume.ContentSource.CloneID)
				must.Eq(t, tc.ContentSource.SnapshotID, resp.Volume.ContentSource.SnapshotID)
			}
			if tc.Response != nil && tc.Response.Volume != nil {
				must.SliceLen(t, 1, resp.Volume.AccessibleTopology)
				must.Eq(t,
					req.AccessibilityRequirements.Requisite[0].Segments,
					resp.Volume.AccessibleTopology[0].Segments,
				)
			}

		})
	}
}

func TestClient_RPC_ControllerDeleteVolume(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name        string
		Request     *ControllerDeleteVolumeRequest
		ResponseErr error
		ExpectedErr error
	}{
		{
			Name:        "handles underlying grpc errors",
			Request:     &ControllerDeleteVolumeRequest{ExternalVolumeID: "vol-12345"},
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},

		{
			Name:        "handles error missing volume ID",
			Request:     &ControllerDeleteVolumeRequest{},
			ExpectedErr: errors.New("missing ExternalVolumeID"),
		},

		{
			Name:    "handles success",
			Request: &ControllerDeleteVolumeRequest{ExternalVolumeID: "vol-12345"},
		},
	}
	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, cc, _, client := newTestClient(t)
			defer client.Close()

			cc.NextErr = tc.ResponseErr
			err := client.ControllerDeleteVolume(context.TODO(), tc.Request)
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
				return
			}
			must.NoError(t, err, must.Sprint("name", tc.Name))
		})
	}
}

func TestClient_RPC_ControllerListVolume(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name        string
		Request     *ControllerListVolumesRequest
		ResponseErr error
		ExpectedErr error
	}{
		{
			Name:        "handles underlying grpc errors",
			Request:     &ControllerListVolumesRequest{},
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},

		{
			Name:        "handles error invalid max entries",
			Request:     &ControllerListVolumesRequest{MaxEntries: -1},
			ExpectedErr: errors.New("MaxEntries cannot be negative"),
		},

		{
			Name:    "handles success",
			Request: &ControllerListVolumesRequest{},
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, cc, _, client := newTestClient(t)
			defer client.Close()

			cc.NextErr = tc.ResponseErr
			if tc.ResponseErr != nil {
				// note: there's nothing interesting to assert here other than
				// that we don't throw a NPE during transformation from
				// protobuf to our struct
				cc.NextListVolumesResponse = &csipbv1.ListVolumesResponse{
					Entries: []*csipbv1.ListVolumesResponse_Entry{
						{
							Volume: &csipbv1.Volume{
								CapacityBytes: 1000000,
								VolumeId:      "vol-0",
								VolumeContext: map[string]string{"foo": "bar"},

								ContentSource: &csipbv1.VolumeContentSource{},
								AccessibleTopology: []*csipbv1.Topology{
									{
										Segments: map[string]string{"rack": "A"},
									},
								},
							},
						},

						{
							Volume: &csipbv1.Volume{
								VolumeId: "vol-1",
								AccessibleTopology: []*csipbv1.Topology{
									{
										Segments: map[string]string{"rack": "A"},
									},
								},
							},
						},

						{
							Volume: &csipbv1.Volume{
								VolumeId: "vol-3",
								ContentSource: &csipbv1.VolumeContentSource{
									Type: &csipbv1.VolumeContentSource_Snapshot{
										Snapshot: &csipbv1.VolumeContentSource_SnapshotSource{
											SnapshotId: "snap-12345",
										},
									},
								},
							},
						},
					},
					NextToken: "abcdef",
				}
			}

			resp, err := client.ControllerListVolumes(context.TODO(), tc.Request)
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
				return
			}
			must.NoError(t, err, must.Sprint("name", tc.Name))
			must.NotNil(t, resp)

		})
	}
}

func TestClient_RPC_ControllerCreateSnapshot(t *testing.T) {
	ci.Parallel(t)

	now := time.Now()

	cases := []struct {
		Name        string
		Request     *ControllerCreateSnapshotRequest
		Response    *csipbv1.CreateSnapshotResponse
		ResponseErr error
		ExpectedErr error
	}{
		{
			Name: "handles underlying grpc errors",
			Request: &ControllerCreateSnapshotRequest{
				VolumeID: "vol-12345",
				Name:     "snap-12345",
			},
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},

		{
			Name:        "handles error missing volume ID",
			Request:     &ControllerCreateSnapshotRequest{},
			ExpectedErr: errors.New("missing VolumeID"),
		},

		{
			Name: "handles success",
			Request: &ControllerCreateSnapshotRequest{
				VolumeID: "vol-12345",
				Name:     "snap-12345",
			},
			Response: &csipbv1.CreateSnapshotResponse{
				Snapshot: &csipbv1.Snapshot{
					SizeBytes:      100000,
					SnapshotId:     "snap-12345",
					SourceVolumeId: "vol-12345",
					CreationTime:   timestamppb.New(now),
					ReadyToUse:     true,
				},
			},
		},
	}
	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, cc, _, client := newTestClient(t)
			defer client.Close()

			cc.NextErr = tc.ResponseErr
			cc.NextCreateSnapshotResponse = tc.Response
			// note: there's nothing interesting to assert about the response
			// here other than that we don't throw a NPE during transformation
			// from protobuf to our struct
			resp, err := client.ControllerCreateSnapshot(context.TODO(), tc.Request)
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
			} else {
				must.NoError(t, err, must.Sprint("name", tc.Name))
				must.Positive(t, resp.Snapshot.CreateTime)
				must.Eq(t, now.Second(), time.Unix(resp.Snapshot.CreateTime, 0).Second())
			}
		})
	}
}

func TestClient_RPC_ControllerDeleteSnapshot(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name        string
		Request     *ControllerDeleteSnapshotRequest
		ResponseErr error
		ExpectedErr error
	}{
		{
			Name:        "handles underlying grpc errors",
			Request:     &ControllerDeleteSnapshotRequest{SnapshotID: "vol-12345"},
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},

		{
			Name:        "handles error missing volume ID",
			Request:     &ControllerDeleteSnapshotRequest{},
			ExpectedErr: errors.New("missing SnapshotID"),
		},

		{
			Name:    "handles success",
			Request: &ControllerDeleteSnapshotRequest{SnapshotID: "vol-12345"},
		},
	}
	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, cc, _, client := newTestClient(t)
			defer client.Close()

			cc.NextErr = tc.ResponseErr
			err := client.ControllerDeleteSnapshot(context.TODO(), tc.Request)
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
				return
			}
			must.NoError(t, err, must.Sprint("name", tc.Name))
		})
	}
}

func TestClient_RPC_ControllerListSnapshots(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name        string
		Request     *ControllerListSnapshotsRequest
		ResponseErr error
		ExpectedErr error
	}{
		{
			Name:        "handles underlying grpc errors",
			Request:     &ControllerListSnapshotsRequest{},
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: fmt.Errorf("controller plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},

		{
			Name:        "handles error invalid max entries",
			Request:     &ControllerListSnapshotsRequest{MaxEntries: -1},
			ExpectedErr: errors.New("MaxEntries cannot be negative"),
		},

		{
			Name:    "handles success",
			Request: &ControllerListSnapshotsRequest{},
		},
	}

	now := time.Now()

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, cc, _, client := newTestClient(t)
			defer client.Close()

			cc.NextErr = tc.ResponseErr
			if tc.ResponseErr == nil {
				cc.NextListSnapshotsResponse = &csipbv1.ListSnapshotsResponse{
					Entries: []*csipbv1.ListSnapshotsResponse_Entry{
						{
							Snapshot: &csipbv1.Snapshot{
								SizeBytes:      1000000,
								SnapshotId:     "snap-12345",
								SourceVolumeId: "vol-12345",
								ReadyToUse:     true,
								CreationTime:   timestamppb.New(now),
							},
						},
					},
					NextToken: "abcdef",
				}
			}

			resp, err := client.ControllerListSnapshots(context.TODO(), tc.Request)
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
				return
			}
			must.NoError(t, err, must.Sprint("name", tc.Name))
			must.NotNil(t, resp)
			must.Len(t, 1, resp.Entries)
			must.Positive(t, resp.Entries[0].Snapshot.CreateTime)
			must.Eq(t, now.Second(),
				time.Unix(resp.Entries[0].Snapshot.CreateTime, 0).Second())
		})
	}
}

func TestClient_RPC_ControllerExpandVolume(t *testing.T) {

	cases := []struct {
		Name        string
		Request     *ControllerExpandVolumeRequest
		ExpectCall  *csipbv1.ControllerExpandVolumeRequest
		ResponseErr error
		ExpectedErr error
	}{
		{
			Name: "success",
			Request: &ControllerExpandVolumeRequest{
				ExternalVolumeID: "vol-1",
				RequiredBytes:    1,
				LimitBytes:       2,
				Capability: &VolumeCapability{
					AccessMode: VolumeAccessModeMultiNodeSingleWriter,
				},
				Secrets: map[string]string{"super": "secret"},
			},
			ExpectCall: &csipbv1.ControllerExpandVolumeRequest{
				VolumeId: "vol-1",
				CapacityRange: &csipbv1.CapacityRange{
					RequiredBytes: 1,
					LimitBytes:    2,
				},
				VolumeCapability: &csipbv1.VolumeCapability{
					AccessMode: &csipbv1.VolumeCapability_AccessMode{
						Mode: csipbv1.VolumeCapability_AccessMode_Mode(csipbv1.VolumeCapability_AccessMode_MULTI_NODE_SINGLE_WRITER),
					},
					AccessType: &csipbv1.VolumeCapability_Block{Block: &csipbv1.VolumeCapability_BlockVolume{}},
				},
				Secrets: map[string]string{"super": "secret"},
			},
		},

		{
			Name: "validate only min set",
			Request: &ControllerExpandVolumeRequest{
				ExternalVolumeID: "vol-1",
				RequiredBytes:    4,
			},
			ExpectCall: &csipbv1.ControllerExpandVolumeRequest{
				VolumeId: "vol-1",
				CapacityRange: &csipbv1.CapacityRange{
					RequiredBytes: 4,
				},
			},
		},
		{
			Name:        "validate missing volume ID",
			Request:     &ControllerExpandVolumeRequest{},
			ExpectedErr: errors.New("missing ExternalVolumeID"),
		},
		{
			Name: "validate missing max/min size",
			Request: &ControllerExpandVolumeRequest{
				ExternalVolumeID: "vol-1",
			},
			ExpectedErr: errors.New("one of LimitBytes or RequiredBytes must be set"),
		},
		{
			Name: "validate min greater than max",
			Request: &ControllerExpandVolumeRequest{
				ExternalVolumeID: "vol-1",
				RequiredBytes:    4,
				LimitBytes:       2,
			},
			ExpectedErr: errors.New("LimitBytes cannot be less than RequiredBytes"),
		},

		{
			Name: "grpc error InvalidArgument",
			Request: &ControllerExpandVolumeRequest{
				ExternalVolumeID: "vol-1", LimitBytes: 1000},
			ResponseErr: status.Errorf(codes.InvalidArgument, "sad args"),
			ExpectedErr: errors.New("requested capabilities not compatible with volume \"vol-1\": rpc error: code = InvalidArgument desc = sad args"),
		},

		{
			Name: "grpc error NotFound",
			Request: &ControllerExpandVolumeRequest{
				ExternalVolumeID: "vol-1", LimitBytes: 1000},
			ResponseErr: status.Errorf(codes.NotFound, "does not exist"),
			ExpectedErr: errors.New("volume \"vol-1\" could not be found: rpc error: code = NotFound desc = does not exist"),
		},
		{
			Name: "grpc error FailedPrecondition",
			Request: &ControllerExpandVolumeRequest{
				ExternalVolumeID: "vol-1", LimitBytes: 1000},
			ResponseErr: status.Errorf(codes.FailedPrecondition, "unsupported"),
			ExpectedErr: errors.New("volume \"vol-1\" cannot be expanded online: rpc error: code = FailedPrecondition desc = unsupported"),
		},
		{
			Name: "grpc error OutOfRange",
			Request: &ControllerExpandVolumeRequest{
				ExternalVolumeID: "vol-1", LimitBytes: 1000},
			ResponseErr: status.Errorf(codes.OutOfRange, "too small"),
			ExpectedErr: errors.New("unsupported capacity_range for volume \"vol-1\": rpc error: code = OutOfRange desc = too small"),
		},
		{
			Name: "grpc error Internal",
			Request: &ControllerExpandVolumeRequest{
				ExternalVolumeID: "vol-1", LimitBytes: 1000},
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: errors.New("controller plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},
		{
			Name: "grpc error default case",
			Request: &ControllerExpandVolumeRequest{
				ExternalVolumeID: "vol-1", LimitBytes: 1000},
			ResponseErr: status.Errorf(codes.DataLoss, "misc unspecified error"),
			ExpectedErr: errors.New("controller plugin returned an error: rpc error: code = DataLoss desc = misc unspecified error"),
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, cc, _, client := newTestClient(t)

			cc.NextErr = tc.ResponseErr
			// the fake client should take ~no time, but set a timeout just in case
			ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
			defer cancel()
			resp, err := client.ControllerExpandVolume(ctx, tc.Request)
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
				return
			}
			must.NoError(t, err)
			must.NotNil(t, resp)
			must.Eq(t, tc.ExpectCall, cc.LastExpandVolumeRequest)

		})
	}

	t.Run("connection error", func(t *testing.T) {
		c := &client{} // induce c.ensureConnected() error
		ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
		defer cancel()
		resp, err := c.ControllerExpandVolume(ctx, &ControllerExpandVolumeRequest{
			ExternalVolumeID: "valid-id",
			RequiredBytes:    1,
		})
		must.Nil(t, resp)
		must.EqError(t, err, "address is empty")
	})
}

func TestClient_RPC_NodeStageVolume(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name        string
		ResponseErr error
		Response    *csipbv1.NodeStageVolumeResponse
		ExpectedErr error
	}{
		{
			Name:        "handles underlying grpc errors",
			ResponseErr: status.Errorf(codes.AlreadyExists, "some grpc error"),
			ExpectedErr: fmt.Errorf("volume \"foo\" is already staged to \"/path\" but with incompatible capabilities for this request: rpc error: code = AlreadyExists desc = some grpc error"),
		},
		{
			Name:        "handles success",
			ResponseErr: nil,
			ExpectedErr: nil,
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, _, nc, client := newTestClient(t)
			defer client.Close()

			nc.NextErr = tc.ResponseErr
			nc.NextStageVolumeResponse = tc.Response

			err := client.NodeStageVolume(context.TODO(), &NodeStageVolumeRequest{
				ExternalID:        "foo",
				StagingTargetPath: "/path",
				VolumeCapability:  &VolumeCapability{},
			})
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
			} else {
				must.NoError(t, err)
			}
		})
	}
}

func TestClient_RPC_NodeUnstageVolume(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name        string
		ResponseErr error
		Response    *csipbv1.NodeUnstageVolumeResponse
		ExpectedErr error
	}{
		{
			Name:        "handles underlying grpc errors",
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: fmt.Errorf("node plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},
		{
			Name:        "handles success",
			ResponseErr: nil,
			ExpectedErr: nil,
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, _, nc, client := newTestClient(t)
			defer client.Close()

			nc.NextErr = tc.ResponseErr
			nc.NextUnstageVolumeResponse = tc.Response

			err := client.NodeUnstageVolume(context.TODO(), "foo", "/foo")
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
			} else {
				must.NoError(t, err)
			}
		})
	}
}

func TestClient_RPC_NodePublishVolume(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name        string
		Request     *NodePublishVolumeRequest
		ResponseErr error
		Response    *csipbv1.NodePublishVolumeResponse
		ExpectedErr error
	}{
		{
			Name: "handles underlying grpc errors",
			Request: &NodePublishVolumeRequest{
				ExternalID:       "foo",
				TargetPath:       "/dev/null",
				VolumeCapability: &VolumeCapability{},
			},
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: fmt.Errorf("node plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},
		{
			Name: "handles success",
			Request: &NodePublishVolumeRequest{
				ExternalID:       "foo",
				TargetPath:       "/dev/null",
				VolumeCapability: &VolumeCapability{},
			},
			ResponseErr: nil,
			ExpectedErr: nil,
		},
		{
			Name: "Performs validation of the publish volume request",
			Request: &NodePublishVolumeRequest{
				ExternalID: "",
			},
			ResponseErr: nil,
			ExpectedErr: errors.New("validation error: missing volume ID"),
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, _, nc, client := newTestClient(t)
			defer client.Close()

			nc.NextErr = tc.ResponseErr
			nc.NextPublishVolumeResponse = tc.Response

			err := client.NodePublishVolume(context.TODO(), tc.Request)
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
			} else {
				must.NoError(t, err)
			}
		})
	}
}
func TestClient_RPC_NodeUnpublishVolume(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name        string
		ExternalID  string
		TargetPath  string
		ResponseErr error
		Response    *csipbv1.NodeUnpublishVolumeResponse
		ExpectedErr error
	}{
		{
			Name:        "handles underlying grpc errors",
			ExternalID:  "foo",
			TargetPath:  "/dev/null",
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: fmt.Errorf("node plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},
		{
			Name:        "handles success",
			ExternalID:  "foo",
			TargetPath:  "/dev/null",
			ResponseErr: nil,
			ExpectedErr: nil,
		},
		{
			Name:        "Performs validation of the request args - ExternalID",
			ResponseErr: nil,
			ExpectedErr: errors.New("missing volumeID"),
		},
		{
			Name:        "Performs validation of the request args - TargetPath",
			ExternalID:  "foo",
			ResponseErr: nil,
			ExpectedErr: errors.New("missing targetPath"),
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, _, nc, client := newTestClient(t)
			defer client.Close()

			nc.NextErr = tc.ResponseErr
			nc.NextUnpublishVolumeResponse = tc.Response

			err := client.NodeUnpublishVolume(context.TODO(), tc.ExternalID, tc.TargetPath)
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
			} else {
				must.NoError(t, err)
			}
		})
	}
}

func TestClient_RPC_NodeExpandVolume(t *testing.T) {
	// minimum valid request
	minRequest := &NodeExpandVolumeRequest{
		ExternalVolumeID: "test-vol",
		TargetPath:       "/test-path",
	}

	cases := []struct {
		Name        string
		Request     *NodeExpandVolumeRequest
		ExpectCall  *csipbv1.NodeExpandVolumeRequest
		ResponseErr error
		ExpectedErr error
	}{
		{
			Name:    "success min",
			Request: minRequest,
			ExpectCall: &csipbv1.NodeExpandVolumeRequest{
				VolumeId:   "test-vol",
				VolumePath: "/test-path",
			},
		},
		{
			Name: "success full",
			Request: &NodeExpandVolumeRequest{
				ExternalVolumeID: "test-vol",
				TargetPath:       "/test-path",
				StagingPath:      "/test-staging-path",
				CapacityRange: &CapacityRange{
					RequiredBytes: 5,
					LimitBytes:    10,
				},
				Capability: &VolumeCapability{
					AccessType: VolumeAccessTypeMount,
					AccessMode: VolumeAccessModeMultiNodeSingleWriter,
					MountVolume: &structs.CSIMountOptions{
						FSType:     "test-fstype",
						MountFlags: []string{"test-flags"},
					},
				},
			},
			ExpectCall: &csipbv1.NodeExpandVolumeRequest{
				VolumeId:          "test-vol",
				VolumePath:        "/test-path",
				StagingTargetPath: "/test-staging-path",
				CapacityRange: &csipbv1.CapacityRange{
					RequiredBytes: 5,
					LimitBytes:    10,
				},
				VolumeCapability: &csipbv1.VolumeCapability{
					AccessType: &csipbv1.VolumeCapability_Mount{
						Mount: &csipbv1.VolumeCapability_MountVolume{
							FsType:           "test-fstype",
							MountFlags:       []string{"test-flags"},
							VolumeMountGroup: "",
						}},
					AccessMode: &csipbv1.VolumeCapability_AccessMode{
						Mode: csipbv1.VolumeCapability_AccessMode_MULTI_NODE_SINGLE_WRITER},
				},
			},
		},

		{
			Name: "validate missing volume id",
			Request: &NodeExpandVolumeRequest{
				TargetPath: "/test-path",
			},
			ExpectedErr: errors.New("ExternalVolumeID is required"),
		},
		{
			Name: "validate missing target path",
			Request: &NodeExpandVolumeRequest{
				ExternalVolumeID: "test-volume",
			},
			ExpectedErr: errors.New("TargetPath is required"),
		},
		{
			Name: "validate min greater than max",
			Request: &NodeExpandVolumeRequest{
				ExternalVolumeID: "test-vol",
				TargetPath:       "/test-path",
				CapacityRange: &CapacityRange{
					RequiredBytes: 4,
					LimitBytes:    2,
				},
			},
			ExpectedErr: errors.New("LimitBytes cannot be less than RequiredBytes"),
		},

		{
			Name:        "grpc error default case",
			Request:     minRequest,
			ResponseErr: status.Errorf(codes.DataLoss, "misc unspecified error"),
			ExpectedErr: errors.New("node plugin returned an error: rpc error: code = DataLoss desc = misc unspecified error"),
		},
		{
			Name:        "grpc error invalid argument",
			Request:     minRequest,
			ResponseErr: status.Errorf(codes.InvalidArgument, "sad args"),
			ExpectedErr: errors.New("requested capabilities not compatible with volume \"test-vol\": rpc error: code = InvalidArgument desc = sad args"),
		},
		{
			Name:        "grpc error NotFound",
			Request:     minRequest,
			ResponseErr: status.Errorf(codes.NotFound, "does not exist"),
			ExpectedErr: errors.New("CSI client error (ignorable): volume \"test-vol\" could not be found: rpc error: code = NotFound desc = does not exist"),
		},
		{
			Name:        "grpc error FailedPrecondition",
			Request:     minRequest,
			ResponseErr: status.Errorf(codes.FailedPrecondition, "unsupported"),
			ExpectedErr: errors.New("volume \"test-vol\" cannot be expanded while in use: rpc error: code = FailedPrecondition desc = unsupported"),
		},
		{
			Name:        "grpc error OutOfRange",
			Request:     minRequest,
			ResponseErr: status.Errorf(codes.OutOfRange, "too small"),
			ExpectedErr: errors.New("unsupported capacity_range for volume \"test-vol\": rpc error: code = OutOfRange desc = too small"),
		},
		{
			Name:        "grpc error Internal",
			Request:     minRequest,
			ResponseErr: status.Errorf(codes.Internal, "some grpc error"),
			ExpectedErr: errors.New("node plugin returned an internal error, check the plugin allocation logs for more information: rpc error: code = Internal desc = some grpc error"),
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			_, _, nc, client := newTestClient(t)

			nc.NextErr = tc.ResponseErr
			// the fake client should take ~no time, but set a timeout just in case
			ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
			defer cancel()
			resp, err := client.NodeExpandVolume(ctx, tc.Request)
			if tc.ExpectedErr != nil {
				must.EqError(t, err, tc.ExpectedErr.Error())
				return
			}
			must.NoError(t, err)
			must.NotNil(t, resp)
			must.Eq(t, tc.ExpectCall, nc.LastExpandVolumeRequest)

		})
	}

	t.Run("connection error", func(t *testing.T) {
		c := &client{} // induce c.ensureConnected() error
		ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
		defer cancel()
		resp, err := c.NodeExpandVolume(ctx, &NodeExpandVolumeRequest{
			ExternalVolumeID: "valid-id",
			TargetPath:       "/some-path",
		})
		must.Nil(t, resp)
		must.EqError(t, err, "address is empty")
	})
}
