From d24b54e8b62a06b1dff73dba09f4fa5ccaa94fb0 Mon Sep 17 00:00:00 2001 From: Maxim Baz Date: Wed, 18 Apr 2018 22:52:34 +0200 Subject: [PATCH] Add unit tests for parsing browser requests, enable Travis (#15) --- .travis.yml | 4 ++ Makefile | 2 +- request/configure.go | 2 +- request/fetch.go | 6 +-- request/list.go | 2 +- request/process.go | 57 +++++++++++--------- request/process_test.go | 115 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 157 insertions(+), 31 deletions(-) create mode 100644 .travis.yml create mode 100644 request/process_test.go diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9075e62 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: go + +install: + - go get -u github.com/golang/dep/cmd/dep diff --git a/Makefile b/Makefile index 0c88414..3f9c9bc 100644 --- a/Makefile +++ b/Makefile @@ -25,4 +25,4 @@ browserpass-freebsd64: *.go **/*.go .PHONY: test test: - go test + go test ./... diff --git a/request/configure.go b/request/configure.go index 2217279..85c4684 100644 --- a/request/configure.go +++ b/request/configure.go @@ -11,7 +11,7 @@ import ( log "github.com/sirupsen/logrus" ) -func configure(request request) { +func configure(request *request) { responseData := response.MakeConfigureResponse() // Check that each and every store in the settings exists and is accessible. diff --git a/request/fetch.go b/request/fetch.go index 6bf7d41..dce62d1 100644 --- a/request/fetch.go +++ b/request/fetch.go @@ -14,7 +14,7 @@ import ( log "github.com/sirupsen/logrus" ) -func fetchDecryptedContents(request request) { +func fetchDecryptedContents(request *request) { responseData := response.MakeFetchResponse() if !strings.HasSuffix(request.File, ".gpg") { @@ -97,7 +97,7 @@ func fetchDecryptedContents(request request) { } } - responseData.Contents, err = decryptFile(store, request.File, gpgPath) + responseData.Contents, err = decryptFile(&store, request.File, gpgPath) if err != nil { log.Errorf( "Unable to decrypt the password file '%v' in the password store '%v' located in '%v': %+v", @@ -141,7 +141,7 @@ func validateGpgBinary(gpgPath string) error { return exec.Command(gpgPath, "--version").Run() } -func decryptFile(store store, file string, gpgPath string) (string, error) { +func decryptFile(store *store, file string, gpgPath string) (string, error) { passwordFilePath := filepath.Join(store.Path, file) passwordFile, err := os.Open(passwordFilePath) if err != nil { diff --git a/request/list.go b/request/list.go index 144ab39..5983f48 100644 --- a/request/list.go +++ b/request/list.go @@ -10,7 +10,7 @@ import ( log "github.com/sirupsen/logrus" ) -func listFiles(request request) { +func listFiles(request *request) { responseData := response.MakeListResponse() for _, store := range request.Settings.Stores { diff --git a/request/process.go b/request/process.go index fe77b93..8bb5031 100644 --- a/request/process.go +++ b/request/process.go @@ -30,8 +30,29 @@ type request struct { // Process handles browser request func Process() { - requestLength := parseRequestLength() - request := parseRequest(requestLength) + requestLength, err := parseRequestLength(os.Stdin) + if err != nil { + log.Error("Unable to parse the length of the browser request: ", err) + response.SendErrorAndExit( + errors.CodeParseRequestLength, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to parse the length of the browser request", + errors.FieldError: err.Error(), + }, + ) + } + + request, err := parseRequest(requestLength, os.Stdin) + if err != nil { + log.Error("Unable to parse the browser request: ", err) + response.SendErrorAndExit( + errors.CodeParseRequest, + &map[errors.Field]string{ + errors.FieldMessage: "Unable to parse the browser request", + errors.FieldError: err.Error(), + }, + ) + } switch request.Action { case "configure": @@ -52,35 +73,21 @@ func Process() { } } -// Request length is the first 4 bytes in LittleEndian encoding on stdin -func parseRequestLength() uint32 { +// Request length is the first 4 bytes in LittleEndian encoding +func parseRequestLength(input io.Reader) (uint32, error) { var length uint32 - if err := binary.Read(os.Stdin, binary.LittleEndian, &length); err != nil { - log.Error("Unable to parse the length of the browser request: ", err) - response.SendErrorAndExit( - errors.CodeParseRequestLength, - &map[errors.Field]string{ - errors.FieldMessage: "Unable to parse the length of the browser request", - errors.FieldError: err.Error(), - }, - ) + if err := binary.Read(input, binary.LittleEndian, &length); err != nil { + return 0, err } - return length + return length, nil } // Request is a json with a predefined structure -func parseRequest(messageLength uint32) request { +func parseRequest(messageLength uint32, input io.Reader) (*request, error) { var parsed request - reader := &io.LimitedReader{R: os.Stdin, N: int64(messageLength)} + reader := &io.LimitedReader{R: input, N: int64(messageLength)} if err := json.NewDecoder(reader).Decode(&parsed); err != nil { - log.Error("Unable to parse the browser request: ", err) - response.SendErrorAndExit( - errors.CodeParseRequest, - &map[errors.Field]string{ - errors.FieldMessage: "Unable to parse the browser request", - errors.FieldError: err.Error(), - }, - ) + return nil, err } - return parsed + return &parsed, nil } diff --git a/request/process_test.go b/request/process_test.go new file mode 100644 index 0000000..8763079 --- /dev/null +++ b/request/process_test.go @@ -0,0 +1,115 @@ +package request + +import ( + "bytes" + "encoding/json" + "io" + "reflect" + "testing" +) + +func Test_ParseRequestLength_ConsidersFirstFourBytes(t *testing.T) { + // Arrange + expected := uint32(201334791) // 0x0c002007 + + // The first 4 bytes represent the value of `expected` in Little Endian format, + // the rest should be completely ignored during the parsing. + input := bytes.NewReader([]byte{7, 32, 0, 12, 13, 13, 13}) + + // Act + actual, err := parseRequestLength(input) + + // Assert + if err != nil { + t.Fatalf("Error parsing request length: %v", err) + } + + if expected != actual { + t.Fatalf("The actual length '%v' does not match the expected value of '%v'", actual, expected) + } +} + +func Test_ParseRequestLength_ConnectionAborted(t *testing.T) { + // Arrange + expectedErr := io.ErrUnexpectedEOF + input := bytes.NewReader([]byte{7}) + + // Act + _, err := parseRequestLength(input) + + // Assert + if expectedErr != err { + t.Fatalf("The expected error is '%v', but got '%v'", expectedErr, err) + } +} + +func Test_ParseRequest_CanParse(t *testing.T) { + // Arrange + expected := &request{ + Action: "list", + Settings: settings{ + Stores: map[string]store{ + "default": store{ + Name: "default", + Path: "~/.password-store", + }, + }, + }, + } + + jsonBytes, err := json.Marshal(expected) + if err != nil { + t.Fatal("Unable to marshal the expected object to initialize the test") + } + + inputLength := uint32(len(jsonBytes)) + input := bytes.NewReader(jsonBytes) + + // Act + actual, err := parseRequest(inputLength, input) + + // Assert + if err != nil { + t.Fatalf("Error parsing request: %v", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("The request was parsed incorrectly.\nExpected: %+v\nActual: %+v", expected, actual) + } +} + +func Test_ParseRequest_WrongLength(t *testing.T) { + // Arrange + expectedErr := io.ErrUnexpectedEOF + + jsonBytes, err := json.Marshal(&request{Action: "list"}) + if err != nil { + t.Fatal("Unable to marshal the expected object to initialize the test") + } + + wrongInputLength := uint32(len(jsonBytes)) - 1 + input := bytes.NewReader(jsonBytes) + + // Act + _, err = parseRequest(wrongInputLength, input) + + // Assert + if expectedErr != err { + t.Fatalf("The expected error is '%v', but got '%v'", expectedErr, err) + } +} + +func Test_ParseRequest_InvalidJson(t *testing.T) { + // Arrange + jsonBytes := []byte("not_a_json") + inputLength := uint32(len(jsonBytes)) + input := bytes.NewReader(jsonBytes) + + // Act + _, err := parseRequest(inputLength, input) + + // Assert + if err == nil { + t.Fatalf("Expected a parsing error, but didn't get it") + } +}