diff --git a/cmd/invoke.go b/cmd/invoke.go index 9ff96d37fa..d36e286d5e 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -179,6 +179,15 @@ func runInvoke(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err return fmt.Errorf("--extension flag is only valid with cloudevent format") } } + // If --file was specified, read the file contents into cfg.Data now + // that all config (including interactive --confirm prompts) is finalized. + if cfg.File != "" { + content, err := os.ReadFile(cfg.File) + if err != nil { + return err + } + cfg.Data = content + } // Client instance from env vars, flags, args and user prompts (if --confirm) client, done := newClient(ClientConfig{Verbose: cfg.Verbose, InsecureSkipVerify: cfg.Insecure}) @@ -197,15 +206,6 @@ func runInvoke(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err Extensions: cfg.extensionsMap(), } - // If --file was specified, use its content for message data - if cfg.File != "" { - content, err := os.ReadFile(cfg.File) - if err != nil { - return err - } - m.Data = content - } - // Invoke metadata, body, err := client.Invoke(cmd.Context(), cfg.Path, cfg.Target, m) if err != nil { @@ -276,15 +276,6 @@ func newInvokeConfig() (cfg invokeConfig, err error) { Extensions: viper.GetStringSlice("extension"), } - // If file was passed, read it in as data - if cfg.File != "" { - b, err := os.ReadFile(cfg.File) - if err != nil { - return cfg, err - } - cfg.Data = b - } - switch strings.ToLower(cfg.Format) { case "cloudevent", "cloudevents": cfg.Format = "cloudevent" diff --git a/cmd/invoke_test.go b/cmd/invoke_test.go index 7feae43f03..0601de26b2 100644 --- a/cmd/invoke_test.go +++ b/cmd/invoke_test.go @@ -4,9 +4,13 @@ import ( "context" "errors" "fmt" + "io" "net" "net/http" "os" + "path/filepath" + "strings" + "sync" "sync/atomic" "testing" "time" @@ -78,3 +82,105 @@ func TestInvoke(t *testing.T) { t.Fatal("function was not invoked") } } + +// TestInvoke_FileFlag ensures that when --file is provided, the file's +// contents are read and sent as the invocation data. +func TestInvoke_FileFlag(t *testing.T) { + root := FromTempDirectory(t) + + // Create a test function to be invoked + if _, err := fn.New().Init(fn.Function{Runtime: "go", Root: root}); err != nil { + t.Fatal(err) + } + + // Create a test data file + testData := "custom file content for invoke test" + testFile := filepath.Join(root, "testdata.txt") + if err := os.WriteFile(testFile, []byte(testData), 0644); err != nil { + t.Fatal(err) + } + + // Track what data is received by the mock server + var ( + receivedData []byte + mu sync.Mutex + ) + + // Mock Runner: starts a service which captures request body + runner := mock.NewRunner() + runner.RunFn = func(ctx context.Context, f fn.Function, _ string, _ time.Duration) (job *fn.Job, err error) { + var ( + l net.Listener + h = http.NewServeMux() + s = http.Server{Handler: h} + ) + if l, err = net.Listen("tcp4", "127.0.0.1:"); err != nil { + t.Fatal(err) + } + h.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { + body, _ := io.ReadAll(req.Body) + mu.Lock() + receivedData = body + mu.Unlock() + _, _ = res.Write([]byte("ok")) + }) + go func() { + if err = s.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) { + fmt.Fprintf(os.Stderr, "error serving: %v", err) + } + }() + host, port, _ := net.SplitHostPort(l.Addr().String()) + errs := make(chan error, 10) + stop := func() error { _ = s.Close(); return nil } + return fn.NewJob(f, host, port, errs, stop, false) + } + + // Run the mock http service + f, err := fn.NewFunction(root) + if err != nil { + t.Fatal(err) + } + client := fn.New(fn.WithRunner(runner)) + job, err := client.Run(t.Context(), f) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = job.Stop() }) + + // Test that the invoke command reads and sends the file content + cmd := NewInvokeCmd(NewClient) + cmd.SetArgs([]string{"--file", testFile, "--content-type", "text/plain"}) + + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + mu.Lock() + got := string(receivedData) + mu.Unlock() + if got != testData { + t.Fatalf("expected file content %q to be sent, got %q", testData, got) + } +} + +// TestInvoke_FileFlagNonExistent ensures that specifying a non-existent +// file via --file returns an appropriate error. +func TestInvoke_FileFlagNonExistent(t *testing.T) { + root := FromTempDirectory(t) + + // Create a test function + if _, err := fn.New().Init(fn.Function{Runtime: "go", Root: root}); err != nil { + t.Fatal(err) + } + + cmd := NewInvokeCmd(NewClient) + cmd.SetArgs([]string{"--file", "nonexistent_file.txt"}) + err := cmd.Execute() + + if err == nil { + t.Fatal("invoking with a nonexistent file should error") + } + if !strings.Contains(err.Error(), "nonexistent_file.txt") { + t.Fatalf("error should mention the missing file, got: %v", err) + } +}