diff --git a/cmd/dirstalk/main.go b/cmd/dirstalk/main.go index 274649b..0d5a0af 100644 --- a/cmd/dirstalk/main.go +++ b/cmd/dirstalk/main.go @@ -32,7 +32,13 @@ func createCommand(logger *logrus.Logger) (*cobra.Command, error) { return nil, errors.Wrap(err, "failed to create scan command") } + resultViewCmd, err := cmd.NewResultViewCommand(logger.Out) + if err != nil { + return nil, errors.Wrap(err, "failed to create result-view command") + } + dirStalkCmd.AddCommand(scanCmd) + dirStalkCmd.AddCommand(resultViewCmd) dirStalkCmd.AddCommand(cmd.NewGenerateDictionaryCommand(logger.Out)) dirStalkCmd.AddCommand(cmd.NewVersionCommand(logger.Out)) diff --git a/functional-tests.sh b/functional-tests.sh index b940e58..1879c74 100755 --- a/functional-tests.sh +++ b/functional-tests.sh @@ -89,3 +89,10 @@ assert_not_contains "$SCAN_RESULT" "error" "no error is expected when priting sc DICTIONARY_GENERATE_RESULT=$(./dist/dirstalk dictionary.generate resources/tests 2>&1 || true); assert_contains "$DICTIONARY_GENERATE_RESULT" "dictionary.txt" "dictionary generation should contains a file in the folder" assert_not_contains "$DICTIONARY_GENERATE_RESULT" "error" "no error is expected when generating a dictionary successfully" + +RESULT_VIEW_RESULT=$(./dist/dirstalk result.view -r resources/tests/out.txt 2>&1 || true); +assert_contains "$RESULT_VIEW_RESULT" "├── adview" "result output should contain tree output" +assert_contains "$RESULT_VIEW_RESULT" "├── partners" "result output should contain tree output" +assert_contains "$RESULT_VIEW_RESULT" "│ └── terms" "result output should contain tree output" +assert_contains "$RESULT_VIEW_RESULT" "└── s" "result output should contain tree output" +assert_not_contains "$RESULT_VIEW_RESULT" "error" "no error is expected when displaying a result" diff --git a/pkg/cmd/flags.go b/pkg/cmd/flags.go index 3702508..8a9c8fc 100644 --- a/pkg/cmd/flags.go +++ b/pkg/cmd/flags.go @@ -26,4 +26,8 @@ const ( flagOutput = "out" flagOutputShort = "o" flagAbsolutePathOnly = "absolute-only" + + // Result view flags + flagResultFile = "result-file" + flagResultFileShort = "r" ) diff --git a/pkg/cmd/result_view.go b/pkg/cmd/result_view.go new file mode 100644 index 0000000..10e24d4 --- /dev/null +++ b/pkg/cmd/result_view.go @@ -0,0 +1,51 @@ +package cmd + +import ( + "io" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/stefanoj3/dirstalk/pkg/result" + "github.com/stefanoj3/dirstalk/pkg/scan/summarizer/tree" +) + +func NewResultViewCommand(out io.Writer) (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "result.view", + Short: "Read a scan output file and render the folder tree", + RunE: buildResultViewCmd(out), + } + + cmd.Flags().StringP( + flagResultFile, + flagResultFileShort, + "", + "result file to read", + ) + err := cmd.MarkFlagFilename(flagResultFile) + if err != nil { + return nil, err + } + + err = cmd.MarkFlagRequired(flagResultFile) + if err != nil { + return nil, err + } + + return cmd, nil +} + +func buildResultViewCmd(out io.Writer) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + resultFilePath := cmd.Flag(flagResultFile).Value.String() + + results, err := result.LoadResultsFromFile(resultFilePath) + if err != nil { + return errors.Wrapf(err, "failed to load results from %s", resultFilePath) + } + + tree.NewResultTreePrinter().Print(results, out) + + return nil + } +} diff --git a/pkg/cmd/result_view_integration_test.go b/pkg/cmd/result_view_integration_test.go new file mode 100644 index 0000000..db1838e --- /dev/null +++ b/pkg/cmd/result_view_integration_test.go @@ -0,0 +1,55 @@ +package cmd_test + +import ( + "testing" + + "github.com/stefanoj3/dirstalk/pkg/common/test" + "github.com/stretchr/testify/assert" +) + +func TestResultViewShouldErrWhenCalledWithoutResultFlag(t *testing.T) { + logger, _ := test.NewLogger() + + c, err := createCommand(logger) + assert.NoError(t, err) + assert.NotNil(t, c) + + _, _, err = executeCommand(c, "result.view") + assert.Error(t, err) + + assert.Contains(t, err.Error(), "result-file") + assert.Contains(t, err.Error(), "not set") +} + +func TestResultViewShouldErrWhenCalledWithInvalidPath(t *testing.T) { + logger, _ := test.NewLogger() + + c, err := createCommand(logger) + assert.NoError(t, err) + assert.NotNil(t, c) + + _, _, err = executeCommand(c, "result.view", "-r", "/root/123/abc") + assert.Error(t, err) + + assert.Contains(t, err.Error(), "failed to load results from") +} + +func TestResultView(t *testing.T) { + logger, loggerBuffer := test.NewLogger() + + c, err := createCommand(logger) + assert.NoError(t, err) + assert.NotNil(t, c) + + _, _, err = executeCommand(c, "result.view", "-r", "testdata/out.txt") + assert.NoError(t, err) + + expected := `/ +├── adview +├── partners +│ └── terms +└── s +` + + assert.Contains(t, loggerBuffer.String(), expected) +} diff --git a/pkg/cmd/root_integration_test.go b/pkg/cmd/root_integration_test.go index fbde245..2c99324 100644 --- a/pkg/cmd/root_integration_test.go +++ b/pkg/cmd/root_integration_test.go @@ -77,7 +77,13 @@ func createCommand(logger *logrus.Logger) (*cobra.Command, error) { return nil, errors.Wrap(err, "failed to create scan command") } + resultViewCommand, err := cmd.NewResultViewCommand(logger.Out) + if err != nil { + return nil, errors.Wrap(err, "failed to create result.view command") + } + dirStalkCmd.AddCommand(scanCmd) + dirStalkCmd.AddCommand(resultViewCommand) dirStalkCmd.AddCommand(cmd.NewGenerateDictionaryCommand(logger.Out)) dirStalkCmd.AddCommand(cmd.NewVersionCommand(logger.Out)) diff --git a/pkg/cmd/testdata/out.txt b/pkg/cmd/testdata/out.txt new file mode 100644 index 0000000..dec7b28 --- /dev/null +++ b/pkg/cmd/testdata/out.txt @@ -0,0 +1,4 @@ +{"Target":{"Path":"partners","Method":"GET","Depth":3},"StatusCode":200,"URL":{"Scheme":"https","Opaque":"","User":null,"Host":"www.brucewillisdiesinarmageddon.co.de","Path":"/partners","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":""}} +{"Target":{"Path":"s","Method":"GET","Depth":3},"StatusCode":400,"URL":{"Scheme":"https","Opaque":"","User":null,"Host":"www.brucewillisdiesinarmageddon.co.de","Path":"/s","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":""}} +{"Target":{"Path":"adview","Method":"GET","Depth":3},"StatusCode":204,"URL":{"Scheme":"https","Opaque":"","User":null,"Host":"www.brucewillisdiesinarmageddon.co.de","Path":"/adview","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":""}} +{"Target":{"Path":"partners/terms","Method":"GET","Depth":2},"StatusCode":200,"URL":{"Scheme":"https","Opaque":"","User":null,"Host":"www.brucewillisdiesinarmageddon.co.de","Path":"/partners/terms","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":""}} diff --git a/pkg/result/load.go b/pkg/result/load.go new file mode 100644 index 0000000..7a31f69 --- /dev/null +++ b/pkg/result/load.go @@ -0,0 +1,50 @@ +package result + +import ( + "bufio" + "encoding/json" + "os" + + "github.com/pkg/errors" + "github.com/stefanoj3/dirstalk/pkg/scan" +) + +func LoadResultsFromFile(resultFilePath string) ([]scan.Result, error) { + file, err := os.Open(resultFilePath) // #nosec + if err != nil { + return nil, errors.Wrapf(err, "failed to open %s", resultFilePath) + } + + defer file.Close() + + fileInfo, err := file.Stat() + if err != nil { + return nil, errors.Wrapf(err, "failed to read properties of %s", resultFilePath) + } + + if fileInfo.IsDir() { + return nil, errors.Errorf("`%s` is a directory, you need to specify a valid result file", resultFilePath) + } + + fileScanner := bufio.NewScanner(file) + + lineCounter := 0 + results := make([]scan.Result, 0, 10) + for fileScanner.Scan() { + lineCounter++ + + r := scan.Result{} + err := json.Unmarshal(fileScanner.Bytes(), &r) + if err != nil { + return nil, errors.Wrapf(err, "unable to read line %d", lineCounter) + } + + results = append(results, r) + } + + if err := fileScanner.Err(); err != nil { + return nil, errors.Wrap(err, "an error occurred while reading the result file") + } + + return results, nil +} diff --git a/pkg/result/load_integration_darwin_test.go b/pkg/result/load_integration_darwin_test.go new file mode 100644 index 0000000..7eb42f6 --- /dev/null +++ b/pkg/result/load_integration_darwin_test.go @@ -0,0 +1,15 @@ +package result_test + +import ( + "testing" + + "github.com/stefanoj3/dirstalk/pkg/result" + "github.com/stretchr/testify/assert" +) + +func TestLoadResultsFromFileShouldErrForInvalidPath(t *testing.T) { + _, err := result.LoadResultsFromFile("/root/123/abc") + assert.Error(t, err) + + assert.Contains(t, err.Error(), "no such file or directory") +} diff --git a/pkg/result/load_integration_linux_test.go b/pkg/result/load_integration_linux_test.go new file mode 100644 index 0000000..a70af32 --- /dev/null +++ b/pkg/result/load_integration_linux_test.go @@ -0,0 +1,15 @@ +package result_test + +import ( + "testing" + + "github.com/stefanoj3/dirstalk/pkg/result" + "github.com/stretchr/testify/assert" +) + +func TestLoadResultsFromFileShouldErrForInvalidPath(t *testing.T) { + _, err := result.LoadResultsFromFile("/root/123/abc") + assert.Error(t, err) + + assert.Contains(t, err.Error(), "permission denied") +} diff --git a/pkg/result/load_integration_test.go b/pkg/result/load_integration_test.go new file mode 100644 index 0000000..9d9ad2f --- /dev/null +++ b/pkg/result/load_integration_test.go @@ -0,0 +1,74 @@ +package result_test + +import ( + "net/url" + "testing" + + "github.com/stefanoj3/dirstalk/pkg/result" + "github.com/stefanoj3/dirstalk/pkg/scan" + "github.com/stretchr/testify/assert" +) + +func TestLoadResultsFromFile(t *testing.T) { + results, err := result.LoadResultsFromFile("testdata/out.txt") + assert.NoError(t, err) + + expectedResults := []scan.Result{ + scan.Result{ + Target: scan.Target{Path: "partners", Method: "GET", Depth: 3}, + StatusCode: 200, + URL: url.URL{ + Scheme: "https", + User: (*url.Userinfo)(nil), + Host: "www.brucewillisdiesinarmageddon.co.de", + Path: "/partners", + }, + }, + scan.Result{ + Target: scan.Target{Path: "s", Method: "GET", Depth: 3}, + StatusCode: 400, + URL: url.URL{ + Scheme: "https", + User: (*url.Userinfo)(nil), + Host: "www.brucewillisdiesinarmageddon.co.de", + Path: "/s", + }, + }, + scan.Result{ + Target: scan.Target{Path: "adview", Method: "GET", Depth: 3}, + StatusCode: 204, + URL: url.URL{ + Scheme: "https", + User: (*url.Userinfo)(nil), + Host: "www.brucewillisdiesinarmageddon.co.de", + Path: "/adview", + }, + }, + scan.Result{ + Target: scan.Target{Path: "partners/terms", Method: "GET", Depth: 2}, + StatusCode: 200, + URL: url.URL{ + Scheme: "https", + User: (*url.Userinfo)(nil), + Host: "www.brucewillisdiesinarmageddon.co.de", + Path: "/partners/terms", + }, + }, + } + + assert.Equal(t, expectedResults, results) +} + +func TestLoadResultsFromFileShouldErrForDirectories(t *testing.T) { + _, err := result.LoadResultsFromFile("testdata/") + assert.Error(t, err) + + assert.Contains(t, err.Error(), "is a directory") +} + +func TestLoadResultsFromFileShouldErrForInvalidFileFormat(t *testing.T) { + _, err := result.LoadResultsFromFile("testdata/invalidout.txt") + assert.Error(t, err) + + assert.Contains(t, err.Error(), "unable to read line") +} diff --git a/pkg/result/testdata/invalidout.txt b/pkg/result/testdata/invalidout.txt new file mode 100644 index 0000000..77b2a6b --- /dev/null +++ b/pkg/result/testdata/invalidout.txt @@ -0,0 +1 @@ +{omg/ \ No newline at end of file diff --git a/pkg/result/testdata/out.txt b/pkg/result/testdata/out.txt new file mode 100644 index 0000000..dec7b28 --- /dev/null +++ b/pkg/result/testdata/out.txt @@ -0,0 +1,4 @@ +{"Target":{"Path":"partners","Method":"GET","Depth":3},"StatusCode":200,"URL":{"Scheme":"https","Opaque":"","User":null,"Host":"www.brucewillisdiesinarmageddon.co.de","Path":"/partners","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":""}} +{"Target":{"Path":"s","Method":"GET","Depth":3},"StatusCode":400,"URL":{"Scheme":"https","Opaque":"","User":null,"Host":"www.brucewillisdiesinarmageddon.co.de","Path":"/s","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":""}} +{"Target":{"Path":"adview","Method":"GET","Depth":3},"StatusCode":204,"URL":{"Scheme":"https","Opaque":"","User":null,"Host":"www.brucewillisdiesinarmageddon.co.de","Path":"/adview","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":""}} +{"Target":{"Path":"partners/terms","Method":"GET","Depth":2},"StatusCode":200,"URL":{"Scheme":"https","Opaque":"","User":null,"Host":"www.brucewillisdiesinarmageddon.co.de","Path":"/partners/terms","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":""}} diff --git a/resources/tests/out.txt b/resources/tests/out.txt new file mode 100644 index 0000000..dec7b28 --- /dev/null +++ b/resources/tests/out.txt @@ -0,0 +1,4 @@ +{"Target":{"Path":"partners","Method":"GET","Depth":3},"StatusCode":200,"URL":{"Scheme":"https","Opaque":"","User":null,"Host":"www.brucewillisdiesinarmageddon.co.de","Path":"/partners","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":""}} +{"Target":{"Path":"s","Method":"GET","Depth":3},"StatusCode":400,"URL":{"Scheme":"https","Opaque":"","User":null,"Host":"www.brucewillisdiesinarmageddon.co.de","Path":"/s","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":""}} +{"Target":{"Path":"adview","Method":"GET","Depth":3},"StatusCode":204,"URL":{"Scheme":"https","Opaque":"","User":null,"Host":"www.brucewillisdiesinarmageddon.co.de","Path":"/adview","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":""}} +{"Target":{"Path":"partners/terms","Method":"GET","Depth":2},"StatusCode":200,"URL":{"Scheme":"https","Opaque":"","User":null,"Host":"www.brucewillisdiesinarmageddon.co.de","Path":"/partners/terms","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":""}}