Ctrl K
ComunidadGuías…

api/handler/service_vm_power_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// capability_id: rainbond.vm-power.start-existing-or-create
// capability_id: rainbond.vm-power.direct-ops-event-close
// capability_id: rainbond.vm-power.stop-restoring-fallback
package handler

import (
	"context"
	"errors"
	"testing"

	"github.com/golang/mock/gomock"
	apimodel "github.com/goodrain/rainbond/api/model"
	ctxutil "github.com/goodrain/rainbond/api/util/ctx"
	"github.com/goodrain/rainbond/db"
	dbmodel "github.com/goodrain/rainbond/db/model"
	mqpb "github.com/goodrain/rainbond/mq/api/grpc/pb"
	mqclient "github.com/goodrain/rainbond/mq/client"
	"google.golang.org/grpc"
	k8sapierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	kubevirtv1 "kubevirt.io/api/core/v1"
	kubecli "kubevirt.io/client-go/kubecli"
)

type recordingMQClient struct {
	tasks []mqclient.TaskStruct
}

func (m *recordingMQClient) Enqueue(context.Context, *mqpb.EnqueueRequest, ...grpc.CallOption) (*mqpb.TaskReply, error) {
	return nil, nil
}

func (m *recordingMQClient) Topics(context.Context, *mqpb.TopicRequest, ...grpc.CallOption) (*mqpb.TaskReply, error) {
	return nil, nil
}

func (m *recordingMQClient) Dequeue(context.Context, *mqpb.DequeueRequest, ...grpc.CallOption) (*mqpb.TaskMessage, error) {
	return nil, nil
}

func (m *recordingMQClient) Close() {}

func (m *recordingMQClient) SendBuilderTopic(t mqclient.TaskStruct) error {
	m.tasks = append(m.tasks, t)
	return nil
}

func newVMOperationEventContext(eventID string) context.Context {
	return context.WithValue(context.Background(), ctxutil.ContextKey("event"), &dbmodel.ServiceEvent{EventID: eventID})
}

func TestStartOrCreateVMStartsExistingStoppedVM(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
	mq := &recordingMQClient{}

	mockClient.EXPECT().VirtualMachine("").Return(mockVMInterface)
	mockVMInterface.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
		Items: []kubevirtv1.VirtualMachine{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "demo-vm",
					Namespace: "demo-ns",
				},
				Status: kubevirtv1.VirtualMachineStatus{
					PrintableStatus: kubevirtv1.VirtualMachineStatusStopped,
				},
			},
		},
	}, nil)
	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Start(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	action := &ServiceAction{MQClient: mq, kubevirtClient: mockClient}
	err := action.StartOrCreateVM(context.Background(), &apimodel.StartStopStruct{
		TenantID:  "tenant-1",
		ServiceID: "service-1",
		EventID:   "event-1",
		TaskType:  "start",
	}, "deploy-v1")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(mq.tasks) != 0 {
		t.Fatalf("expected no worker start task, got %#v", mq.tasks)
	}
}

func TestStartOrCreateVMStartsExistingStoppedVMWhenSpecSyncFindsMissingRootImport(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)

	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Start(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	action := &ServiceAction{
		kubevirtClient: mockClient,
		syncVirtualMachineSpecHook: func(serviceID string) error {
			return errors.New("validate vm boot path failure: missing imported root data volume for vm boot source format \"qcow2\"")
		},
		getVirtualMachineByServiceIDHook: func(serviceID string) (*kubevirtv1.VirtualMachine, error) {
			return &kubevirtv1.VirtualMachine{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "demo-vm",
					Namespace: "demo-ns",
				},
				Status: kubevirtv1.VirtualMachineStatus{
					PrintableStatus: kubevirtv1.VirtualMachineStatusStopped,
				},
			}, nil
		},
	}

	err := action.StartOrCreateVM(context.Background(), &apimodel.StartStopStruct{
		TenantID:  "tenant-1",
		ServiceID: "service-1",
		EventID:   "event-1",
		TaskType:  "start",
	}, "deploy-v1")
	if err != nil {
		t.Fatalf("expected existing legacy vm to start despite missing root import sync error, got %v", err)
	}
}

func TestStartOrCreateVMStartsStoppedAlwaysRunStrategyVM(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{eventDao: eventDao})
	defer db.SetTestManager(nil)

	runStrategy := kubevirtv1.RunStrategyAlways

	mockClient.EXPECT().VirtualMachine("").Return(mockVMInterface)
	mockVMInterface.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
		Items: []kubevirtv1.VirtualMachine{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "demo-vm",
					Namespace: "demo-ns",
				},
				Spec: kubevirtv1.VirtualMachineSpec{
					RunStrategy: &runStrategy,
				},
				Status: kubevirtv1.VirtualMachineStatus{
					PrintableStatus: kubevirtv1.VirtualMachineStatusStopped,
				},
			},
		},
	}, nil)
	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Start(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	action := &ServiceAction{kubevirtClient: mockClient}
	err := action.StartOrCreateVM(newVMOperationEventContext("event-1"), &apimodel.StartStopStruct{
		TenantID:  "tenant-1",
		ServiceID: "service-1",
		EventID:   "event-1",
		TaskType:  "start",
	}, "deploy-v1")
	if err != nil {
		t.Fatalf("expected stopped Always run strategy VM to start, got %v", err)
	}
	if len(eventDao.statuses) != 1 || eventDao.statuses[0] != dbmodel.EventStatusSuccess {
		t.Fatalf("expected success event status update, got %#v", eventDao.statuses)
	}
}

func TestStartOrCreateVMStartsExistingVMWithUnknownPrintableStatus(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)

	mockClient.EXPECT().VirtualMachine("").Return(mockVMInterface)
	mockVMInterface.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
		Items: []kubevirtv1.VirtualMachine{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "demo-vm",
					Namespace: "demo-ns",
				},
			},
		},
	}, nil)
	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Start(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	action := &ServiceAction{kubevirtClient: mockClient}
	err := action.StartOrCreateVM(context.Background(), &apimodel.StartStopStruct{
		TenantID:  "tenant-1",
		ServiceID: "service-1",
		EventID:   "event-1",
		TaskType:  "start",
	}, "deploy-v1")
	if err != nil {
		t.Fatalf("expected VM with unknown printable status to start, got %v", err)
	}
}

func TestStartOrCreateVMMarksDirectStartEventSuccess(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{eventDao: eventDao})
	defer db.SetTestManager(nil)

	mockClient.EXPECT().VirtualMachine("").Return(mockVMInterface)
	mockVMInterface.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
		Items: []kubevirtv1.VirtualMachine{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "demo-vm",
					Namespace: "demo-ns",
				},
				Status: kubevirtv1.VirtualMachineStatus{
					PrintableStatus: kubevirtv1.VirtualMachineStatusStopped,
				},
			},
		},
	}, nil)
	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Start(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	action := &ServiceAction{kubevirtClient: mockClient}
	err := action.StartOrCreateVM(newVMOperationEventContext("event-1"), &apimodel.StartStopStruct{
		TenantID:  "tenant-1",
		ServiceID: "service-1",
		EventID:   "event-1",
		TaskType:  "start",
	}, "deploy-v1")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(eventDao.statuses) != 1 || eventDao.statuses[0] != dbmodel.EventStatusSuccess {
		t.Fatalf("expected success event status update, got %#v", eventDao.statuses)
	}
}

func TestStartOrCreateVMMarksDirectStartEventFailure(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{eventDao: eventDao})
	defer db.SetTestManager(nil)

	mockClient.EXPECT().VirtualMachine("").Return(mockVMInterface)
	mockVMInterface.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
		Items: []kubevirtv1.VirtualMachine{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "demo-vm",
					Namespace: "demo-ns",
				},
				Status: kubevirtv1.VirtualMachineStatus{
					PrintableStatus: kubevirtv1.VirtualMachineStatusStopped,
				},
			},
		},
	}, nil)
	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Start(gomock.Any(), "demo-vm", gomock.Any()).Return(errors.New("boom"))

	action := &ServiceAction{kubevirtClient: mockClient}
	err := action.StartOrCreateVM(newVMOperationEventContext("event-1"), &apimodel.StartStopStruct{
		TenantID:  "tenant-1",
		ServiceID: "service-1",
		EventID:   "event-1",
		TaskType:  "start",
	}, "deploy-v1")
	if err == nil {
		t.Fatal("expected direct VM start failure to be returned")
	}
	if len(eventDao.statuses) != 1 || eventDao.statuses[0] != dbmodel.EventStatusFailure {
		t.Fatalf("expected failure event status update, got %#v", eventDao.statuses)
	}
}

func TestStartOrCreateVMTreatsTransientStartConflictAsSuccessAfterRefresh(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{eventDao: eventDao})
	defer db.SetTestManager(nil)

	originalDelay := vmStartRetryDelay
	vmStartRetryDelay = 0
	defer func() {
		vmStartRetryDelay = originalDelay
	}()

	gomock.InOrder(
		mockClient.EXPECT().VirtualMachine("").Return(mockVMInterface),
		mockVMInterface.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
			Items: []kubevirtv1.VirtualMachine{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "demo-vm",
						Namespace: "demo-ns",
					},
					Status: kubevirtv1.VirtualMachineStatus{
						PrintableStatus: kubevirtv1.VirtualMachineStatusStopped,
					},
				},
			},
		}, nil),
		mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface),
		mockVMInterface.EXPECT().Start(gomock.Any(), "demo-vm", gomock.Any()).Return(
			k8sapierrors.NewConflict(schema.GroupResource{Group: "kubevirt.io", Resource: "virtualmachines"}, "demo-vm", errors.New("conflict")),
		),
		mockClient.EXPECT().VirtualMachine("").Return(mockVMInterface),
		mockVMInterface.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
			Items: []kubevirtv1.VirtualMachine{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "demo-vm",
						Namespace: "demo-ns",
					},
					Status: kubevirtv1.VirtualMachineStatus{
						PrintableStatus: kubevirtv1.VirtualMachineStatusStarting,
					},
				},
			},
		}, nil),
	)

	action := &ServiceAction{kubevirtClient: mockClient}
	err := action.StartOrCreateVM(newVMOperationEventContext("event-1"), &apimodel.StartStopStruct{
		TenantID:  "tenant-1",
		ServiceID: "service-1",
		EventID:   "event-1",
		TaskType:  "start",
	}, "deploy-v1")
	if err != nil {
		t.Fatalf("expected transient conflict to be treated as success after refresh, got %v", err)
	}
	if len(eventDao.statuses) != 1 || eventDao.statuses[0] != dbmodel.EventStatusSuccess {
		t.Fatalf("expected success event status update, got %#v", eventDao.statuses)
	}
}

func TestStartOrCreateVMFallsBackToWorkerStartWhenVMIsMissing(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
	mq := &recordingMQClient{}

	mockClient.EXPECT().VirtualMachine("").Return(mockVMInterface)
	mockVMInterface.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{}, nil)

	action := &ServiceAction{MQClient: mq, kubevirtClient: mockClient}
	err := action.StartOrCreateVM(context.Background(), &apimodel.StartStopStruct{
		TenantID:  "tenant-1",
		ServiceID: "service-1",
		EventID:   "event-1",
		TaskType:  "start",
	}, "deploy-v1")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(mq.tasks) != 1 {
		t.Fatalf("expected one worker start task, got %#v", mq.tasks)
	}
	if mq.tasks[0].TaskType != "start" {
		t.Fatalf("expected worker task type start, got %q", mq.tasks[0].TaskType)
	}
}

func TestStopVMStopsExistingVMWithoutWorkerTask(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
	mq := &recordingMQClient{}

	mockClient.EXPECT().VirtualMachine("").Return(mockVMInterface)
	mockVMInterface.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
		Items: []kubevirtv1.VirtualMachine{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "demo-vm",
					Namespace: "demo-ns",
				},
			},
		},
	}, nil)
	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Stop(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	action := &ServiceAction{MQClient: mq, kubevirtClient: mockClient}
	err := action.StopVM(context.Background(), "service-1")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(mq.tasks) != 0 {
		t.Fatalf("expected no worker stop task, got %#v", mq.tasks)
	}
}

func TestStopVMMarksDirectStopEventSuccess(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{eventDao: eventDao})
	defer db.SetTestManager(nil)

	mockClient.EXPECT().VirtualMachine("").Return(mockVMInterface)
	mockVMInterface.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
		Items: []kubevirtv1.VirtualMachine{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "demo-vm",
					Namespace: "demo-ns",
				},
				Status: kubevirtv1.VirtualMachineStatus{
					PrintableStatus: kubevirtv1.VirtualMachineStatusRunning,
				},
			},
		},
	}, nil)
	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Stop(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	action := &ServiceAction{kubevirtClient: mockClient}
	err := action.StopVM(newVMOperationEventContext("event-1"), "service-1")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(eventDao.statuses) != 1 || eventDao.statuses[0] != dbmodel.EventStatusSuccess {
		t.Fatalf("expected success event status update, got %#v", eventDao.statuses)
	}
}

func TestStopVMFallsBackToHaltingProvisioningVMWhenDirectStopFails(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMList := kubecli.NewMockVirtualMachineInterface(ctrl)
	mockVMUpdate := kubecli.NewMockVirtualMachineInterface(ctrl)

	gomock.InOrder(
		mockClient.EXPECT().VirtualMachine("").Return(mockVMList),
		mockVMList.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
			Items: []kubevirtv1.VirtualMachine{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "demo-vm",
						Namespace: "demo-ns",
					},
					Spec: kubevirtv1.VirtualMachineSpec{
						RunStrategy: pointerToRunStrategy(kubevirtv1.RunStrategyAlways),
					},
					Status: kubevirtv1.VirtualMachineStatus{
						PrintableStatus: kubevirtv1.VirtualMachineStatusProvisioning,
					},
				},
			},
		}, nil),
		mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMUpdate),
		mockVMUpdate.EXPECT().Stop(gomock.Any(), "demo-vm", gomock.Any()).Return(errors.New("stop not allowed while provisioning")),
		mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMUpdate),
		mockVMUpdate.EXPECT().Update(gomock.Any(), gomock.AssignableToTypeOf(&kubevirtv1.VirtualMachine{}), gomock.Any()).DoAndReturn(
			func(_ context.Context, vm *kubevirtv1.VirtualMachine, _ metav1.UpdateOptions) (*kubevirtv1.VirtualMachine, error) {
				if vm.Spec.RunStrategy == nil || *vm.Spec.RunStrategy != kubevirtv1.RunStrategyHalted {
					t.Fatalf("expected fallback update to halt vm, got %#v", vm.Spec.RunStrategy)
				}
				if vm.Spec.Running != nil {
					t.Fatalf("expected fallback update to clear legacy running flag, got %#v", vm.Spec.Running)
				}
				return vm, nil
			},
		),
	)

	action := &ServiceAction{kubevirtClient: mockClient}
	err := action.StopVM(context.Background(), "service-1")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
}

func TestStopVMFallsBackToHaltingDataVolumeErrorVMWhenDirectStopFails(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMList := kubecli.NewMockVirtualMachineInterface(ctrl)
	mockVMUpdate := kubecli.NewMockVirtualMachineInterface(ctrl)

	gomock.InOrder(
		mockClient.EXPECT().VirtualMachine("").Return(mockVMList),
		mockVMList.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
			Items: []kubevirtv1.VirtualMachine{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "demo-vm",
						Namespace: "demo-ns",
					},
					Spec: kubevirtv1.VirtualMachineSpec{
						RunStrategy: pointerToRunStrategy(kubevirtv1.RunStrategyAlways),
					},
					Status: kubevirtv1.VirtualMachineStatus{
						PrintableStatus: kubevirtv1.VirtualMachineStatusDataVolumeError,
					},
				},
			},
		}, nil),
		mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMUpdate),
		mockVMUpdate.EXPECT().Stop(gomock.Any(), "demo-vm", gomock.Any()).Return(errors.New("stop not allowed while datavolume error")),
		mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMUpdate),
		mockVMUpdate.EXPECT().Update(gomock.Any(), gomock.AssignableToTypeOf(&kubevirtv1.VirtualMachine{}), gomock.Any()).DoAndReturn(
			func(_ context.Context, vm *kubevirtv1.VirtualMachine, _ metav1.UpdateOptions) (*kubevirtv1.VirtualMachine, error) {
				if vm.Spec.RunStrategy == nil || *vm.Spec.RunStrategy != kubevirtv1.RunStrategyHalted {
					t.Fatalf("expected fallback update to halt vm, got %#v", vm.Spec.RunStrategy)
				}
				return vm, nil
			},
		),
	)

	action := &ServiceAction{kubevirtClient: mockClient}
	err := action.StopVM(context.Background(), "service-1")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
}

func TestRestartVMRestartsExistingRunningVMWithoutWorkerTask(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
	mq := &recordingMQClient{}

	mockClient.EXPECT().VirtualMachine("").Return(mockVMInterface)
	mockVMInterface.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
		Items: []kubevirtv1.VirtualMachine{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "demo-vm",
					Namespace: "demo-ns",
				},
				Status: kubevirtv1.VirtualMachineStatus{
					PrintableStatus: kubevirtv1.VirtualMachineStatusRunning,
				},
			},
		},
	}, nil)
	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Restart(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	action := &ServiceAction{MQClient: mq, kubevirtClient: mockClient}
	err := action.RestartVM(context.Background(), &apimodel.StartStopStruct{
		TenantID:  "tenant-1",
		ServiceID: "service-1",
		EventID:   "event-1",
		TaskType:  "restart",
	}, "deploy-v1")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(mq.tasks) != 0 {
		t.Fatalf("expected no worker restart task, got %#v", mq.tasks)
	}
}

func TestRestartVMMarksDirectRestartEventSuccess(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{eventDao: eventDao})
	defer db.SetTestManager(nil)

	mockClient.EXPECT().VirtualMachine("").Return(mockVMInterface)
	mockVMInterface.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
		Items: []kubevirtv1.VirtualMachine{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "demo-vm",
					Namespace: "demo-ns",
				},
				Status: kubevirtv1.VirtualMachineStatus{
					PrintableStatus: kubevirtv1.VirtualMachineStatusRunning,
				},
			},
		},
	}, nil)
	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Restart(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	action := &ServiceAction{kubevirtClient: mockClient}
	err := action.RestartVM(newVMOperationEventContext("event-1"), &apimodel.StartStopStruct{
		TenantID:  "tenant-1",
		ServiceID: "service-1",
		EventID:   "event-1",
		TaskType:  "restart",
	}, "deploy-v1")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(eventDao.statuses) != 1 || eventDao.statuses[0] != dbmodel.EventStatusSuccess {
		t.Fatalf("expected success event status update, got %#v", eventDao.statuses)
	}
}

func TestRestartVMStartsStoppedVMWithoutWorkerTask(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
	mq := &recordingMQClient{}

	mockClient.EXPECT().VirtualMachine("").Return(mockVMInterface)
	mockVMInterface.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-1"}).Return(&kubevirtv1.VirtualMachineList{
		Items: []kubevirtv1.VirtualMachine{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "demo-vm",
					Namespace: "demo-ns",
				},
				Status: kubevirtv1.VirtualMachineStatus{
					PrintableStatus: kubevirtv1.VirtualMachineStatusStopped,
				},
			},
		},
	}, nil)
	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Start(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	action := &ServiceAction{MQClient: mq, kubevirtClient: mockClient}
	err := action.RestartVM(context.Background(), &apimodel.StartStopStruct{
		TenantID:  "tenant-1",
		ServiceID: "service-1",
		EventID:   "event-1",
		TaskType:  "restart",
	}, "deploy-v1")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(mq.tasks) != 0 {
		t.Fatalf("expected no worker restart task, got %#v", mq.tasks)
	}
}