diff --git a/internal/cleanup/cleanup_integration_test.go b/internal/cleanup/cleanup_integration_test.go index 8a86836..8a14a68 100644 --- a/internal/cleanup/cleanup_integration_test.go +++ b/internal/cleanup/cleanup_integration_test.go @@ -16,6 +16,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/opdev/virtwork/internal/cleanup" + "github.com/opdev/virtwork/internal/config" "github.com/opdev/virtwork/internal/resources" "github.com/opdev/virtwork/internal/testutil" "github.com/opdev/virtwork/internal/vm" @@ -44,7 +45,7 @@ var _ = Describe("CleanupAll [integration]", func() { opts := testutil.DefaultVMOpts("cleanup-vm-0", namespace) Expect(vm.CreateVM(ctx, c, vm.BuildVMSpec(opts))).To(Succeed()) - result, err := cleanup.CleanupAll(ctx, c, namespace, false, "") + result, err := cleanup.CleanupAll(ctx, c, &config.Config{Namespace: namespace}, false, "") Expect(err).NotTo(HaveOccurred()) Expect(result.VMsDeleted).To(Equal(1)) }) @@ -65,7 +66,7 @@ var _ = Describe("CleanupAll [integration]", func() { } Expect(resources.CreateService(ctx, c, svc)).To(Succeed()) - result, err := cleanup.CleanupAll(ctx, c, namespace, false, "") + result, err := cleanup.CleanupAll(ctx, c, &config.Config{Namespace: namespace}, false, "") Expect(err).NotTo(HaveOccurred()) Expect(result.ServicesDeleted).To(Equal(1)) }) @@ -82,7 +83,7 @@ var _ = Describe("CleanupAll [integration]", func() { ), ).To(Succeed()) - result, err := cleanup.CleanupAll(ctx, c, namespace, false, "") + result, err := cleanup.CleanupAll(ctx, c, &config.Config{Namespace: namespace}, false, "") Expect(err).NotTo(HaveOccurred()) Expect(result.SecretsDeleted).To(Equal(1)) }) @@ -98,7 +99,7 @@ var _ = Describe("CleanupAll [integration]", func() { } Expect(c.Create(ctx, unmanaged)).To(Succeed()) - result, err := cleanup.CleanupAll(ctx, c, namespace, false, "") + result, err := cleanup.CleanupAll(ctx, c, &config.Config{Namespace: namespace}, false, "") Expect(err).NotTo(HaveOccurred()) Expect(result.SecretsDeleted).To(Equal(0)) @@ -108,13 +109,13 @@ var _ = Describe("CleanupAll [integration]", func() { }) It("should delete the namespace when flagged", func() { - result, err := cleanup.CleanupAll(ctx, c, namespace, true, "") + result, err := cleanup.CleanupAll(ctx, c, &config.Config{Namespace: namespace}, true, "") Expect(err).NotTo(HaveOccurred()) Expect(result.NamespaceDeleted).To(BeTrue()) }) It("should not delete the namespace when not flagged", func() { - result, err := cleanup.CleanupAll(ctx, c, namespace, false, "") + result, err := cleanup.CleanupAll(ctx, c, &config.Config{Namespace: namespace}, false, "") Expect(err).NotTo(HaveOccurred()) Expect(result.NamespaceDeleted).To(BeFalse()) @@ -154,7 +155,7 @@ var _ = Describe("CleanupAll [integration]", func() { ), ).To(Succeed()) - result, err := cleanup.CleanupAll(ctx, c, namespace, false, "") + result, err := cleanup.CleanupAll(ctx, c, &config.Config{Namespace: namespace}, false, "") Expect(err).NotTo(HaveOccurred()) Expect(result.VMsDeleted).To(Equal(1)) Expect(result.ServicesDeleted).To(Equal(1)) @@ -163,7 +164,7 @@ var _ = Describe("CleanupAll [integration]", func() { }) It("should handle empty namespace gracefully", func() { - result, err := cleanup.CleanupAll(ctx, c, namespace, false, "") + result, err := cleanup.CleanupAll(ctx, c, &config.Config{Namespace: namespace}, false, "") Expect(err).NotTo(HaveOccurred()) Expect(result.VMsDeleted).To(Equal(0)) Expect(result.ServicesDeleted).To(Equal(0)) @@ -175,7 +176,7 @@ var _ = Describe("CleanupAll [integration]", func() { opts := testutil.DefaultVMOpts("cleanup-idem-vm", namespace) Expect(vm.CreateVM(ctx, c, vm.BuildVMSpec(opts))).To(Succeed()) - result1, err := cleanup.CleanupAll(ctx, c, namespace, false, "") + result1, err := cleanup.CleanupAll(ctx, c, &config.Config{Namespace: namespace}, false, "") Expect(err).NotTo(HaveOccurred()) Expect(result1.VMsDeleted).To(Equal(1)) @@ -186,7 +187,7 @@ var _ = Describe("CleanupAll [integration]", func() { return len(vms) }, 60*time.Second, 2*time.Second).Should(Equal(0)) - result2, err := cleanup.CleanupAll(ctx, c, namespace, false, "") + result2, err := cleanup.CleanupAll(ctx, c, &config.Config{Namespace: namespace}, false, "") Expect(err).NotTo(HaveOccurred()) Expect(result2.VMsDeleted).To(Equal(0)) }) diff --git a/internal/testutil/binary_test.go b/internal/testutil/binary_test.go new file mode 100644 index 0000000..bd33197 --- /dev/null +++ b/internal/testutil/binary_test.go @@ -0,0 +1,122 @@ +// Copyright 2026 Red Hat +// SPDX-License-Identifier: Apache-2.0 + +package testutil_test + +import ( + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/opdev/virtwork/internal/testutil" +) + +var _ = Describe("BinaryPath", func() { + var originalEnv string + + BeforeEach(func() { + originalEnv = os.Getenv("VIRTWORK_BINARY") + }) + + AfterEach(func() { + if originalEnv != "" { + _ = os.Setenv("VIRTWORK_BINARY", originalEnv) + } else { + _ = os.Unsetenv("VIRTWORK_BINARY") + } + }) + + Context("when VIRTWORK_BINARY environment variable is set", func() { + It("should return the path from the environment variable", func() { + expectedPath := "/custom/path/to/virtwork" + _ = os.Setenv("VIRTWORK_BINARY", expectedPath) + + path, err := testutil.BinaryPath() + Expect(err).NotTo(HaveOccurred()) + Expect(path).To(Equal(expectedPath)) + }) + }) + + Context("when VIRTWORK_BINARY environment variable is not set", func() { + BeforeEach(func() { + _ = os.Unsetenv("VIRTWORK_BINARY") + }) + + It("should build and return the binary path", func() { + path, err := testutil.BinaryPath() + Expect(err).NotTo(HaveOccurred()) + Expect(path).NotTo(BeEmpty()) + Expect(path).To(BeAnExistingFile()) + }) + + It("should return a path in a temp directory", func() { + path, err := testutil.BinaryPath() + Expect(err).NotTo(HaveOccurred()) + Expect(filepath.Dir(path)).To(ContainSubstring("virtwork-e2e")) + }) + + It("should cache the built binary on subsequent calls", func() { + path1, err := testutil.BinaryPath() + Expect(err).NotTo(HaveOccurred()) + + path2, err := testutil.BinaryPath() + Expect(err).NotTo(HaveOccurred()) + + Expect(path1).To(Equal(path2)) + }) + }) +}) + +var _ = Describe("RunVirtwork", func() { + Context("when running the virtwork binary with valid arguments", func() { + It("should execute successfully with --help flag", func() { + stdout, _, exitCode, err := testutil.RunVirtwork("--help") + Expect(err).NotTo(HaveOccurred()) + Expect(exitCode).To(Equal(0)) + Expect(stdout).To(ContainSubstring("virtwork")) + Expect(stdout).To(ContainSubstring("Usage:")) + }) + + It("should execute successfully with help command", func() { + stdout, _, exitCode, err := testutil.RunVirtwork("help") + Expect(err).NotTo(HaveOccurred()) + Expect(exitCode).To(Equal(0)) + Expect(stdout).To(ContainSubstring("virtwork")) + }) + + It("should show available commands in help output", func() { + stdout, _, exitCode, err := testutil.RunVirtwork("--help") + Expect(err).NotTo(HaveOccurred()) + Expect(exitCode).To(Equal(0)) + Expect(stdout).To(ContainSubstring("Available Commands")) + Expect(stdout).To(ContainSubstring("run")) + Expect(stdout).To(ContainSubstring("cleanup")) + }) + }) + + Context("when running with invalid arguments", func() { + It("should return non-zero exit code for invalid flags", func() { + _, _, exitCode, err := testutil.RunVirtwork("--invalid-flag-that-does-not-exist") + Expect(err).NotTo(HaveOccurred()) + Expect(exitCode).NotTo(Equal(0)) + }) + + It("should return non-zero exit code for invalid commands", func() { + _, _, exitCode, err := testutil.RunVirtwork("invalid-command") + Expect(err).NotTo(HaveOccurred()) + Expect(exitCode).NotTo(Equal(0)) + }) + }) + + Context("when running without arguments", func() { + It("should show help message", func() { + stdout, _, exitCode, err := testutil.RunVirtwork() + Expect(err).NotTo(HaveOccurred()) + Expect(exitCode).To(Equal(0)) + Expect(stdout).To(ContainSubstring("virtwork")) + Expect(stdout).To(ContainSubstring("Usage:")) + }) + }) +}) diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index 7f56234..d2f6ecf 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -112,12 +112,16 @@ func WaitForVMRunning(ctx context.Context, c client.Client, name, namespace stri if err == nil && phase == "Running" { return nil } - if time.Now().After(deadline) { + remaining := time.Until(deadline) + if remaining <= 0 { if err != nil { return fmt.Errorf("timeout waiting for VM %s to be running: %w", name, err) } return fmt.Errorf("waiting for VM %s to be running (phase: %s); %w", name, phase, wait.ErrVMTimeout) } + if remaining < interval { + interval = remaining + } time.Sleep(interval) } } diff --git a/internal/testutil/testutil_integration_test.go b/internal/testutil/testutil_integration_test.go new file mode 100644 index 0000000..2f39e41 --- /dev/null +++ b/internal/testutil/testutil_integration_test.go @@ -0,0 +1,172 @@ +// Copyright 2026 Red Hat +// SPDX-License-Identifier: Apache-2.0 + +//go:build integration + +package testutil_test + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/opdev/virtwork/internal/testutil" +) + +var _ = Describe("MustConnect", func() { + Context("when connecting to a cluster", func() { + It("should successfully connect with valid kubeconfig", func() { + c := testutil.MustConnect("") + Expect(c).NotTo(BeNil()) + }) + + It("should use KUBECONFIG environment variable when path is empty", func() { + c := testutil.MustConnect("") + Expect(c).NotTo(BeNil()) + + list := &corev1.NamespaceList{} + err := c.List(context.Background(), list) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when given invalid kubeconfig path", func() { + It("should panic on connection failure", func() { + Expect(func() { + testutil.MustConnect("/invalid/path/to/kubeconfig") + }).To(Panic()) + }) + }) +}) + +var _ = Describe("EnsureTestNamespace", func() { + var ( + ctx context.Context + c client.Client + namespace string + ) + + BeforeEach(func() { + ctx = context.Background() + c = testutil.MustConnect("") + namespace = testutil.UniqueNamespace("ensure-test") + }) + + AfterEach(func() { + if namespace != "" { + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + _ = c.Delete(ctx, ns) + } + }) + + Context("when creating a test namespace", func() { + It("should successfully create a namespace", func() { + err := testutil.EnsureTestNamespace(ctx, c, namespace) + Expect(err).NotTo(HaveOccurred()) + + ns := &corev1.Namespace{} + err = c.Get(ctx, client.ObjectKey{Name: namespace}, ns) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should add managed-by labels to the namespace", func() { + err := testutil.EnsureTestNamespace(ctx, c, namespace) + Expect(err).NotTo(HaveOccurred()) + + ns := &corev1.Namespace{} + err = c.Get(ctx, client.ObjectKey{Name: namespace}, ns) + Expect(err).NotTo(HaveOccurred()) + + labels := testutil.ManagedLabels() + for key, value := range labels { + Expect(ns.Labels).To(HaveKeyWithValue(key, value)) + } + }) + + It("should not error if namespace already exists", func() { + err := testutil.EnsureTestNamespace(ctx, c, namespace) + Expect(err).NotTo(HaveOccurred()) + + err = testutil.EnsureTestNamespace(ctx, c, namespace) + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) + +var _ = Describe("CleanupNamespace", func() { + var ( + ctx context.Context + c client.Client + namespace string + ) + + BeforeEach(func() { + ctx = context.Background() + c = testutil.MustConnect("") + namespace = testutil.UniqueNamespace("cleanup-test") + err := testutil.EnsureTestNamespace(ctx, c, namespace) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("when cleaning up a namespace", func() { + It("should not panic on cleanup", func() { + Expect(func() { + testutil.CleanupNamespace(ctx, c, namespace) + }).NotTo(Panic()) + }) + + It("should handle cleanup of non-existent namespace gracefully", func() { + nonexistent := "virtwork-test-nonexistent-12345678" + Expect(func() { + testutil.CleanupNamespace(ctx, c, nonexistent) + }).NotTo(Panic()) + }) + }) +}) + +var _ = Describe("WaitForVMRunning", func() { + var ( + ctx context.Context + c client.Client + namespace string + ) + + BeforeEach(func() { + ctx = context.Background() + c = testutil.MustConnect("") + namespace = testutil.UniqueNamespace("wait-test") + err := testutil.EnsureTestNamespace(ctx, c, namespace) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + if namespace != "" { + testutil.CleanupNamespace(ctx, c, namespace) + } + }) + + Context("when waiting for a non-existent VM", func() { + It("should timeout and return an error", func() { + err := testutil.WaitForVMRunning(ctx, c, "nonexistent-vm", namespace, 5*time.Second) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("timeout")) + }) + }) + + Context("when waiting with a very short timeout", func() { + It("should return error quickly", func() { + start := time.Now() + err := testutil.WaitForVMRunning(ctx, c, "test-vm", namespace, 1*time.Second) + elapsed := time.Since(start) + + Expect(err).To(HaveOccurred()) + Expect(elapsed).To(BeNumerically(">=", 1*time.Second)) + Expect(elapsed).To(BeNumerically("<", 3*time.Second)) + }) + }) +}) diff --git a/internal/testutil/testutil_test.go b/internal/testutil/testutil_test.go new file mode 100644 index 0000000..5ee8791 --- /dev/null +++ b/internal/testutil/testutil_test.go @@ -0,0 +1,122 @@ +// Copyright 2026 Red Hat +// SPDX-License-Identifier: Apache-2.0 + +package testutil_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/opdev/virtwork/internal/constants" + "github.com/opdev/virtwork/internal/testutil" +) + +func TestTestutil(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Testutil Suite") +} + +var _ = Describe("UniqueNamespace", func() { + Context("when generating unique namespace names", func() { + It("should include the prefix in the namespace name", func() { + result := testutil.UniqueNamespace("myprefix") + Expect(result).To(ContainSubstring("myprefix")) + }) + + It("should start with 'virtwork-test-' prefix", func() { + result := testutil.UniqueNamespace("foo") + Expect(result).To(HavePrefix("virtwork-test-")) + }) + + It("should generate unique names on subsequent calls", func() { + name1 := testutil.UniqueNamespace("test") + name2 := testutil.UniqueNamespace("test") + Expect(name1).NotTo(Equal(name2)) + }) + + It("should generate names with expected format", func() { + result := testutil.UniqueNamespace("integration") + Expect(result).To(MatchRegexp(`^virtwork-test-integration-[0-9a-f]{8}$`)) + }) + }) +}) + +var _ = Describe("ManagedLabels", func() { + Context("when retrieving managed labels", func() { + It("should return a map with the managed-by label", func() { + labels := testutil.ManagedLabels() + Expect(labels).To(HaveKey(constants.LabelManagedBy)) + }) + + It("should have the correct managed-by value", func() { + labels := testutil.ManagedLabels() + Expect(labels[constants.LabelManagedBy]).To(Equal(constants.ManagedByValue)) + }) + + It("should return a non-empty map", func() { + labels := testutil.ManagedLabels() + Expect(labels).NotTo(BeEmpty()) + }) + }) +}) + +var _ = Describe("DefaultVMOpts", func() { + var ( + vmName string + vmNamespace string + ) + + BeforeEach(func() { + vmName = "test-vm" + vmNamespace = "test-namespace" + }) + + Context("when creating default VM options", func() { + It("should set the name correctly", func() { + opts := testutil.DefaultVMOpts(vmName, vmNamespace) + Expect(opts.Name).To(Equal(vmName)) + }) + + It("should set the namespace correctly", func() { + opts := testutil.DefaultVMOpts(vmName, vmNamespace) + Expect(opts.Namespace).To(Equal(vmNamespace)) + }) + + It("should use the default container disk image", func() { + opts := testutil.DefaultVMOpts(vmName, vmNamespace) + Expect(opts.ContainerDiskImage).To(Equal(constants.DefaultContainerDiskImage)) + }) + + It("should set 1 CPU core", func() { + opts := testutil.DefaultVMOpts(vmName, vmNamespace) + Expect(opts.CPUCores).To(Equal(1)) + }) + + It("should set 512Mi memory", func() { + opts := testutil.DefaultVMOpts(vmName, vmNamespace) + Expect(opts.Memory).To(Equal("512Mi")) + }) + + It("should include managed-by label", func() { + opts := testutil.DefaultVMOpts(vmName, vmNamespace) + Expect(opts.Labels).To(HaveKeyWithValue(constants.LabelManagedBy, constants.ManagedByValue)) + }) + + It("should include app name label", func() { + opts := testutil.DefaultVMOpts(vmName, vmNamespace) + Expect(opts.Labels).To(HaveKeyWithValue(constants.LabelAppName, "virtwork")) + }) + + It("should include component label set to 'test'", func() { + opts := testutil.DefaultVMOpts(vmName, vmNamespace) + Expect(opts.Labels).To(HaveKeyWithValue(constants.LabelComponent, "test")) + }) + + It("should have a simple cloud-init userdata", func() { + opts := testutil.DefaultVMOpts(vmName, vmNamespace) + Expect(opts.CloudInitUserdata).To(Equal("#cloud-config\n")) + }) + }) +})