1

EDIT: Adrian's suggestion makes sense, so I moved my code into a function and called the function from my cobra block:

package cmd

import (
    "fmt"
    "log"
    "os"
    "io"

    "github.com/spf13/cobra"
    "github.com/spf13/viper"
    input "github.com/tcnksm/go-input"
)

var configureCmd = &cobra.Command{
    Use:   "configure",
    Short: "Configure your TFE credentials",
    Long:  `Prompts for your TFE API credentials, then writes them to
    a configuration file (defaults to ~/.tgc.yaml`,
    Run: func(cmd *cobra.Command, args []string) {
        CreateConfigFileFromPrompts(os.Stdin, os.Stdout)
    },
}

func CreateConfigFileFromPrompts(stdin io.Reader, stdout io.Writer) {
    ui := &input.UI{
        Writer: stdout,
        Reader: stdin,
    }

    tfeURL, err := ui.Ask("TFE URL:", &input.Options{
        Default:  "https://app.terraform.io",
        Required: true,
        Loop:     true,
        })
    if err != nil {
        log.Fatal(err)
    }
    viper.Set("tfe_url", tfeURL)

    tfeAPIToken, err := ui.Ask(fmt.Sprintf("TFE API Token (Create one at %s/app/settings/tokens)", tfeURL), &input.Options{
        Default:     "",
        Required:    true,
        Loop:        true,
        Mask:        true,
        MaskDefault: true,
        })

    if err != nil {
        log.Fatal(err)
    }
    viper.Set("tfe_api_token", tfeAPIToken)

    configPath := ConfigPath()
    viper.SetConfigFile(configPath)

    err = viper.WriteConfig()

    if err != nil {
        log.Fatal("Failed to write to: ", configPath, " Error was: ", err)
    }

    fmt.Println("Saved to", configPath)
}

So what can I pass to this method to test that the output is as expected?

package cmd

import (
  "strings"
  "testing"
)

func TestCreateConfigFileFromPrompts(t *testing.T) {
  // How do I pass the stdin and out to the method?
  // Then how do I test their contents?
  // CreateConfigFileFromPrompts()
}
Peter Souter
  • 5,110
  • 1
  • 33
  • 62
  • 5
    In your test, pass something else - say, a `io.Pipe` or `bytes.Buffer` - to your `input.UI` instead of passing `Stdin` and `Stdout`. – Adrian Jan 03 '19 at 15:00
  • Good point, I moved the logic into a function and the cobra code defaults to `os.Stdin`. How do I test this? – Peter Souter Jan 06 '19 at 17:47

2 Answers2

1
func TestCreateConfigFileFromPrompts(t *testing.T) {

    var in bytes.Buffer
    var gotOut, wantOut bytes.Buffer

    // The reader should read to the \n each of two times.
    in.Write([]byte("example-url.com\nexampletoken\n"))

    // wantOut could just be []byte, but for symmetry's sake I've used another buffer
    wantOut.Write([]byte("TFE URL:TFE API Token (Create one at example-url.com/app/settings/tokens)"))

    // I don't know enough about Viper to manage ConfigPath()
    // but it seems youll have to do it here somehow.
    configFilePath := "test/file/location"

    CreateConfigFileFromPrompts(&in, &gotOut)

    // verify that correct prompts were sent to the writer
    if !bytes.Equal(gotOut.Bytes(), wantOut.Bytes()) {
        t.Errorf("Prompts = %s, want %s", gotOut.Bytes(), wantOut.Bytes())
    }

    // May not need/want to test viper's writing of the config file here, or at all, but if so:
    var fileGot, fileWant []byte
    fileWant = []byte("Correct Config file contents:\n URL:example-url.com\nTOKEN:exampletoken")
    fileGot, err := ioutil.ReadFile(configFilePath)
    if err != nil {
        t.Errorf("Error reading config file %s", configFilePath)
    }
    if !bytes.Equal(fileGot, fileWant) {
        t.Errorf("ConfigFile: %s not created correctly got = %s, want %s", configFilePath, fileGot, fileWant)
    }
}

As highlighted by @zdebra in comments to his answer, the go-input package is panicing and giving you the error: Reader must be a file. If you are married to using that package, you can avoid the problem by disabling the masking option on the ui.Ask for your second input:

tfeAPIToken, err := ui.Ask(fmt.Sprintf("TFE API Token (Create one at %s/app/settings/tokens)", tfeURL), &input.Options{
        Default:     "",
        Required:    true,
        Loop:        true,
        //Mask:        true, // if this is set to True, the input must be a file for some reason
        //MaskDefault: true,
    })
blobdon
  • 1,176
  • 7
  • 11
  • Yep, this makes sense! I do require masking as it's a sensitive value, but I realized I can mock the stdin value for that as a seperate method. – Peter Souter Jan 13 '19 at 16:48
0

The reader and the writer need to be set up before the tested function is called. After is called, the result is written into the writer where it should be verified.

package cmd

import (
  "strings"
  "testing"
)

func TestCreateConfigFileFromPrompts(t *testing.T) {
  in := strings.NewReader("<your input>") // you can use anything that satisfies io.Reader interface here
  out := new(strings.Builder) // you could use anything that satisfies io.Writer interface here like bytes.Buffer

  CreateConfigFileFromPrompts(in, out)

  // here you verify the output written into the out
  expectedOutput := "<your expected output>"
  if out.String() != expectedOutput {
    t.Errorf("expected %s to be equal to %s", out.String(), expectedOutput)
  }
}
zdebra
  • 948
  • 8
  • 22
  • For the reader, my stdin requests asks two questions, so how would I represent that as an `io.Reader` interface? Would I have to defer or something? – Peter Souter Jan 07 '19 at 12:48
  • @PeterSouter depends on the Operating system. See this [answer](https://stackoverflow.com/a/39259747/3348617). If you are on mac or linux just user `\n` as a delimiter for your inputs. – zdebra Jan 07 '19 at 13:49
  • Ah, so it just interprets newlines as each input? That makes it simple – Peter Souter Jan 07 '19 at 15:26
  • When I run this I get `reader must be a file`, do I need to create a new object within the reader? – Peter Souter Jan 07 '19 at 15:27
  • 3
    The library accepts `io.Reader` interface but inside implementation forcing to use `os.File` concrete reader implementation. I would avoid such a library and use standard library tools. [Here](https://tutorialedge.net/golang/reading-console-input-golang/) is a great article about reading user inputs, where I like the `bufio.Scanner` the most. – zdebra Jan 08 '19 at 10:03