package cmd import ( "context" "os" "testing" fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/mock" . "knative.dev/func/pkg/testing" ) // TestDelete_Default ensures that the deployed function is deleted correctly // with default options and the default situation: running "delete" from // within the same directory of the function which is to be deleted. func TestDelete_Default(t *testing.T) { var ( err error root = FromTempDirectory(t) name = "myfunc" namespace = "testns" remover = mock.NewRemover() ctx = context.Background() ) // Remover which confirms the name and namespace received are those // originally requested via the CLI flags. remover.RemoveFn = func(n, ns string) error { if n != name { t.Errorf("expected name '%v', got '%v'", name, n) } if ns != namespace { t.Errorf("expected namespace '%v', got '%v'", namespace, ns) } return nil } // A function which will be created in the requested namespace f := fn.Function{ Runtime: "go", Name: name, Namespace: namespace, Root: root, Registry: TestRegistry, } if _, f, err = fn.New().New(ctx, f); err != nil { t.Fatal(err) } if err = f.Write(); err != nil { t.Fatal(err) } cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover))) cmd.SetArgs([]string{}) if err := cmd.Execute(); err != nil { t.Fatal(err) } // Fail if remover's .Remove not invoked at all if !remover.RemoveInvoked { t.Fatal("fn.Remover not invoked") } } // TestDelete_ByName ensures that running delete specifying the name of the // function explicitly as an argument invokes the remover appropriately. func TestDelete_ByName(t *testing.T) { var ( root = FromTempDirectory(t) testname = "testname" // explicit name for the function testnamespace = "testnamespace" // explicit namespace for the function remover = mock.NewRemover() // with a mock remover err error ) // Remover fails the test if it receives the incorrect name remover.RemoveFn = func(n, _ string) error { if n != testname { t.Fatalf("expected delete name %v, got %v", testname, n) } return nil } f := fn.Function{ Root: root, Runtime: "go", Registry: TestRegistry, Name: "testname", } if f, err = fn.New().Init(f); err != nil { t.Fatal(err) } // simulate deployed function in namespace for the client Remover f.Deploy.Namespace = testnamespace if err = f.Write(); err != nil { t.Fatal(err) } // Create a command with a client constructor fn that instantiates a client // with a mocked remover. cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover))) cmd.SetArgs([]string{testname}) // run: func delete if err := cmd.Execute(); err != nil { t.Fatal(err) } // Fail if remover's .Remove not invoked at all if !remover.RemoveInvoked { t.Fatal("fn.Remover not invoked") } } // TestDelete_Namespace ensures that remover is envoked when --namespace flag is // given --> func delete myfunc --namespace myns func TestDelete_Namespace(t *testing.T) { var ( namespace = "myns" remover = mock.NewRemover() testname = "testname" ) remover.RemoveFn = func(_, ns string) error { if ns != namespace { t.Fatalf("expected delete namespace '%v', got '%v'", namespace, ns) } return nil } cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover))) cmd.SetArgs([]string{testname, "--namespace", namespace}) if err := cmd.Execute(); err != nil { t.Fatal(err) } if !remover.RemoveInvoked { t.Fatal("remover was not invoked") } } // TestDelete_NamespaceFlagPriority ensures that even thought there is // a deployed function the namespace flag takes precedence and essentially // ignores the the function on disk func TestDelete_NamespaceFlagPriority(t *testing.T) { var ( root = FromTempDirectory(t) namespace = "myns" namespace2 = "myns2" remover = mock.NewRemover() testname = "testname" err error ) remover.RemoveFn = func(_, ns string) error { if ns != namespace2 { t.Fatalf("expected delete namespace '%v', got '%v'", namespace2, ns) } return nil } // Ensure the extant function's namespace is used f := fn.Function{ Name: testname, Root: root, Runtime: "go", Registry: TestRegistry, Namespace: namespace, } client := fn.New() _, _, err = client.New(context.Background(), f) if err != nil { t.Fatal(err) } cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover))) cmd.SetArgs([]string{testname, "--namespace", namespace2}) if err := cmd.Execute(); err != nil { t.Fatal(err) } if !remover.RemoveInvoked { t.Fatal("remover was not invoked") } } // TestDelete_NamespaceWithoutNameFails ensures that providing wrong argument // combination fails nice and fast (no name of the Function) func TestDelete_NamespaceWithoutNameFails(t *testing.T) { _ = FromTempDirectory(t) cmd := NewDeleteCmd(NewTestClient()) cmd.SetArgs([]string{"--namespace=myns"}) if err := cmd.Execute(); err == nil { t.Fatal("invoking Delete with namespace BUT without name provided anywhere") } } // TestDelete_ByProject ensures that running delete with a valid project as its // context invokes remove and with the correct name (reads name from func.yaml) func TestDelete_ByProject(t *testing.T) { _ = FromTempDirectory(t) // Write a func.yaml config which specifies a name funcYaml := `name: bar namespace: "func" runtime: go image: "" builder: quay.io/boson/faas-go-builder builders: default: quay.io/boson/faas-go-builder envs: [] annotations: {} labels: [] created: 2021-01-01T00:00:00+00:00 ` if err := os.WriteFile("func.yaml", []byte(funcYaml), 0600); err != nil { t.Fatal(err) } // A mock remover which fails if the name from the func.yaml is not received. remover := mock.NewRemover() remover.RemoveFn = func(n, _ string) error { if n != "bar" { t.Fatalf("expected name 'bar', got '%v'", n) } return nil } // Command with a Client constructor that returns client with the // mocked remover. cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover))) cmd.SetArgs([]string{}) // Do not use test command args // Execute the command simulating no arguments. err := cmd.Execute() if err != nil { t.Fatal(err) } // Also fail if remover's .Remove is not invoked if !remover.RemoveInvoked { t.Fatal("fn.Remover not invoked") } } // TestDelete_ByPath ensures that providing only path deletes the Function // successfully func TestDelete_ByPath(t *testing.T) { var ( // A mock remover which will be sampled to ensure it is not invoked. remover = mock.NewRemover() root = FromTempDirectory(t) err error namespace = "func" ) // Ensure the extant function's namespace is used f := fn.Function{ Root: root, Runtime: "go", Registry: TestRegistry, Deploy: fn.DeploySpec{Namespace: namespace}, } // Initialize a function in temp dir if f, err = fn.New().Init(f); err != nil { t.Fatal(err) } if err = f.Write(); err != nil { t.Fatal(err) } // Command with a Client constructor using the mock remover. cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover))) // Execute the command only with the path argument cmd.SetArgs([]string{"-p", root}) err = cmd.Execute() if err != nil { t.Fatalf("failed with: %v", err) } // Also fail if remover's .Remove is not invoked. if !remover.RemoveInvoked { t.Fatal("fn.Remover not invoked despite valid argument") } } // TestDelete_NameAndPathExclusivity ensures that providing both a name and a // path generates an error. // Providing the --path (-p) flag indicates the name of the function to delete // is to be taken from the function at the given path. Providing the name as // an argument as well is therefore redundant and an error. func TestDelete_NameAndPathExclusivity(t *testing.T) { // A mock remover which will be sampled to ensure it is not invoked. remover := mock.NewRemover() // Command with a Client constructor using the mock remover. cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover))) // Execute the command simulating the invalid argument combination of both // a path and an explicit name. cmd.SetArgs([]string{"-p", "./testpath", "testname"}) err := cmd.Execute() if err == nil { // TODO should really either parse the output or use typed errors to ensure it's // failing for the expected reason. t.Fatalf("expected error on conflicting flags not received") } // Also fail if remover's .Remove is invoked. if remover.RemoveInvoked { t.Fatal("fn.Remover invoked despite invalid combination and an error") } }