summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api4/webhook.go12
-rw-r--r--api4/webhook_test.go10
-rw-r--r--app/command.go5
-rw-r--r--i18n/en.json4
-rw-r--r--model/client.go4
-rw-r--r--model/client4.go8
-rw-r--r--model/command_response.go24
-rw-r--r--model/command_response_test.go206
-rw-r--r--utils/config.go16
-rw-r--r--utils/config_test.go14
-rw-r--r--utils/jsonutils/json.go56
-rw-r--r--utils/jsonutils/json_test.go237
12 files changed, 487 insertions, 109 deletions
diff --git a/api4/webhook.go b/api4/webhook.go
index 52c4ea331..a0e7b5785 100644
--- a/api4/webhook.go
+++ b/api4/webhook.go
@@ -509,11 +509,15 @@ func commandWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
- response := model.CommandResponseFromHTTPBody(r.Header.Get("Content-Type"), r.Body)
-
- err := c.App.HandleCommandWebhook(id, response)
+ response, err := model.CommandResponseFromHTTPBody(r.Header.Get("Content-Type"), r.Body)
if err != nil {
- c.Err = err
+ c.Err = model.NewAppError("commandWebhook", "web.command_webhook.parse.app_error", nil, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ appErr := c.App.HandleCommandWebhook(id, response)
+ if appErr != nil {
+ c.Err = appErr
return
}
diff --git a/api4/webhook_test.go b/api4/webhook_test.go
index 0a295b4b2..e983b6461 100644
--- a/api4/webhook_test.go
+++ b/api4/webhook_test.go
@@ -917,17 +917,21 @@ func TestCommandWebhooks(t *testing.T) {
t.Fatal(err)
}
- if resp, _ := http.Post(Client.Url+"/hooks/commands/123123123123", "application/json", bytes.NewBufferString("{\"text\":\"this is a test\"}")); resp.StatusCode != http.StatusNotFound {
+ if resp, _ := http.Post(Client.Url+"/hooks/commands/123123123123", "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); resp.StatusCode != http.StatusNotFound {
t.Fatal("expected not-found for non-existent hook")
}
+ if resp, err := http.Post(Client.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"invalid`)); err != nil || resp.StatusCode != http.StatusBadRequest {
+ t.Fatal(err)
+ }
+
for i := 0; i < 5; i++ {
- if resp, err := http.Post(Client.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString("{\"text\":\"this is a test\"}")); err != nil || resp.StatusCode != http.StatusOK {
+ if resp, err := http.Post(Client.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); err != nil || resp.StatusCode != http.StatusOK {
t.Fatal(err)
}
}
- if resp, _ := http.Post(Client.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString("{\"text\":\"this is a test\"}")); resp.StatusCode != http.StatusBadRequest {
+ if resp, _ := http.Post(Client.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); resp.StatusCode != http.StatusBadRequest {
t.Fatal("expected error for sixth usage")
}
}
diff --git a/app/command.go b/app/command.go
index fa9b38bf3..039952cf0 100644
--- a/app/command.go
+++ b/app/command.go
@@ -246,8 +246,9 @@ func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *
return nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, err.Error(), http.StatusInternalServerError)
} else {
if resp.StatusCode == http.StatusOK {
- response := model.CommandResponseFromHTTPBody(resp.Header.Get("Content-Type"), resp.Body)
- if response == nil {
+ if response, err := model.CommandResponseFromHTTPBody(resp.Header.Get("Content-Type"), resp.Body); err != nil {
+ return nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, err.Error(), http.StatusInternalServerError)
+ } else if response == nil {
return nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusInternalServerError)
} else {
return a.HandleCommandResponse(cmd, args, response, false)
diff --git a/i18n/en.json b/i18n/en.json
index 9f6d972af..c99d582f2 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -4687,6 +4687,10 @@
"translation": "Invalid user id"
},
{
+ "id": "model.client.command.parse.app_error",
+ "translation": "Unable to parse incoming data"
+ },
+ {
"id": "model.client.connecting.app_error",
"translation": "We encountered an error while connecting to the server"
},
diff --git a/model/client.go b/model/client.go
index b48d1ac37..317374d36 100644
--- a/model/client.go
+++ b/model/client.go
@@ -831,8 +831,10 @@ func (c *Client) Command(channelId string, command string) (*Result, *AppError)
return nil, err
} else {
defer closeBody(r)
+
+ response, _ := CommandResponseFromJson(r.Body)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
- r.Header.Get(HEADER_ETAG_SERVER), CommandResponseFromJson(r.Body)}, nil
+ r.Header.Get(HEADER_ETAG_SERVER), response}, nil
}
}
diff --git a/model/client4.go b/model/client4.go
index 260d75df6..387ca038f 100644
--- a/model/client4.go
+++ b/model/client4.go
@@ -2991,7 +2991,9 @@ func (c *Client4) ExecuteCommand(channelId, command string) (*CommandResponse, *
return nil, BuildErrorResponse(r, err)
} else {
defer closeBody(r)
- return CommandResponseFromJson(r.Body), BuildResponse(r)
+
+ response, _ := CommandResponseFromJson(r.Body)
+ return response, BuildResponse(r)
}
}
@@ -3007,7 +3009,9 @@ func (c *Client4) ExecuteCommandWithTeam(channelId, teamId, command string) (*Co
return nil, BuildErrorResponse(r, err)
} else {
defer closeBody(r)
- return CommandResponseFromJson(r.Body), BuildResponse(r)
+
+ response, _ := CommandResponseFromJson(r.Body)
+ return response, BuildResponse(r)
}
}
diff --git a/model/command_response.go b/model/command_response.go
index cac7e8452..1ed5286de 100644
--- a/model/command_response.go
+++ b/model/command_response.go
@@ -8,6 +8,8 @@ import (
"io"
"io/ioutil"
"strings"
+
+ "github.com/mattermost/mattermost-server/utils/jsonutils"
)
const (
@@ -31,14 +33,14 @@ func (o *CommandResponse) ToJson() string {
return string(b)
}
-func CommandResponseFromHTTPBody(contentType string, body io.Reader) *CommandResponse {
+func CommandResponseFromHTTPBody(contentType string, body io.Reader) (*CommandResponse, error) {
if strings.TrimSpace(strings.Split(contentType, ";")[0]) == "application/json" {
return CommandResponseFromJson(body)
}
if b, err := ioutil.ReadAll(body); err == nil {
- return CommandResponseFromPlainText(string(b))
+ return CommandResponseFromPlainText(string(b)), nil
}
- return nil
+ return nil, nil
}
func CommandResponseFromPlainText(text string) *CommandResponse {
@@ -47,15 +49,19 @@ func CommandResponseFromPlainText(text string) *CommandResponse {
}
}
-func CommandResponseFromJson(data io.Reader) *CommandResponse {
- decoder := json.NewDecoder(data)
- var o CommandResponse
+func CommandResponseFromJson(data io.Reader) (*CommandResponse, error) {
+ b, err := ioutil.ReadAll(data)
+ if err != nil {
+ return nil, err
+ }
- if err := decoder.Decode(&o); err != nil {
- return nil
+ var o CommandResponse
+ err = json.Unmarshal(b, &o)
+ if err != nil {
+ return nil, jsonutils.HumanizeJsonError(err, b)
}
o.Attachments = StringifySlackFieldValue(o.Attachments)
- return &o
+ return &o, nil
}
diff --git a/model/command_response_test.go b/model/command_response_test.go
index dde8d032b..894f9d655 100644
--- a/model/command_response_test.go
+++ b/model/command_response_test.go
@@ -6,17 +6,9 @@ package model
import (
"strings"
"testing"
-)
-
-func TestCommandResponseJson(t *testing.T) {
- o := CommandResponse{Text: "test"}
- json := o.ToJson()
- ro := CommandResponseFromJson(strings.NewReader(json))
- if o.Text != ro.Text {
- t.Fatal("Ids do not match")
- }
-}
+ "github.com/stretchr/testify/assert"
+)
func TestCommandResponseFromHTTPBody(t *testing.T) {
for _, test := range []struct {
@@ -29,95 +21,137 @@ func TestCommandResponseFromHTTPBody(t *testing.T) {
{"application/json", `{"text": "foo"}`, "foo"},
{"application/json; charset=utf-8", `{"text": "foo"}`, "foo"},
} {
- response := CommandResponseFromHTTPBody(test.ContentType, strings.NewReader(test.Body))
- if response.Text != test.ExpectedText {
- t.Fatal()
- }
+ response, err := CommandResponseFromHTTPBody(test.ContentType, strings.NewReader(test.Body))
+ assert.NoError(t, err)
+ assert.Equal(t, test.ExpectedText, response.Text)
}
}
func TestCommandResponseFromPlainText(t *testing.T) {
response := CommandResponseFromPlainText("foo")
- if response.Text != "foo" {
- t.Fatal("text should be foo")
- }
+ assert.Equal(t, "foo", response.Text)
}
func TestCommandResponseFromJson(t *testing.T) {
- json := `{
- "response_type": "ephemeral",
- "text": "response text",
- "username": "response username",
- "icon_url": "response icon url",
- "goto_location": "response goto location",
- "attachments": [{
- "text": "attachment 1 text",
- "pretext": "attachment 1 pretext"
- },{
- "text": "attachment 2 text",
- "fields": [{
- "title": "field 1",
- "value": "value 1",
- "short": true
- },{
- "title": "field 2",
- "value": [],
- "short": false
- }]
- }]
- }`
-
- response := CommandResponseFromJson(strings.NewReader(json))
+ t.Parallel()
- if response == nil {
- t.Fatal("should've received non-nil CommandResponse")
+ sToP := func(s string) *string {
+ return &s
}
- if response.ResponseType != "ephemeral" {
- t.Fatal("should've received correct response type")
- } else if response.Text != "response text" {
- t.Fatal("should've received correct response text")
- } else if response.Username != "response username" {
- t.Fatal("should've received correct response username")
- } else if response.IconURL != "response icon url" {
- t.Fatal("should've received correct response icon url")
- } else if response.GotoLocation != "response goto location" {
- t.Fatal("should've received correct response goto location")
+ testCases := []struct {
+ Description string
+ Json string
+ ExpectedCommandResponse *CommandResponse
+ ExpectedError *string
+ }{
+ {
+ "empty response",
+ "",
+ nil,
+ sToP("parsing error at line 1, character 1: unexpected end of JSON input"),
+ },
+ {
+ "malformed response",
+ `{"text": }`,
+ nil,
+ sToP("parsing error at line 1, character 11: invalid character '}' looking for beginning of value"),
+ },
+ {
+ "invalid response",
+ `{"text": "test", "response_type": 5}`,
+ nil,
+ sToP("parsing error at line 1, character 36: json: cannot unmarshal number into Go struct field CommandResponse.response_type of type string"),
+ },
+ {
+ "ephemeral response",
+ `{
+ "response_type": "ephemeral",
+ "text": "response text",
+ "username": "response username",
+ "icon_url": "response icon url",
+ "goto_location": "response goto location",
+ "attachments": [{
+ "text": "attachment 1 text",
+ "pretext": "attachment 1 pretext"
+ },{
+ "text": "attachment 2 text",
+ "fields": [{
+ "title": "field 1",
+ "value": "value 1",
+ "short": true
+ },{
+ "title": "field 2",
+ "value": [],
+ "short": false
+ }]
+ }]
+ }`,
+ &CommandResponse{
+ ResponseType: "ephemeral",
+ Text: "response text",
+ Username: "response username",
+ IconURL: "response icon url",
+ GotoLocation: "response goto location",
+ Attachments: []*SlackAttachment{
+ {
+ Text: "attachment 1 text",
+ Pretext: "attachment 1 pretext",
+ },
+ {
+ Text: "attachment 2 text",
+ Fields: []*SlackAttachmentField{
+ {
+ Title: "field 1",
+ Value: "value 1",
+ Short: true,
+ },
+ {
+ Title: "field 2",
+ Value: "[]",
+ Short: false,
+ },
+ },
+ },
+ },
+ },
+ nil,
+ },
+ {
+ "null array items",
+ `{"attachments":[{"fields":[{"title":"foo","value":"bar","short":true}, null]}, null]}`,
+ &CommandResponse{
+ Attachments: []*SlackAttachment{
+ {
+ Fields: []*SlackAttachmentField{
+ {
+ Title: "foo",
+ Value: "bar",
+ Short: true,
+ },
+ },
+ },
+ },
+ },
+ nil,
+ },
}
- attachments := response.Attachments
- if len(attachments) != 2 {
- t.Fatal("should've received 2 attachments")
- } else if attachments[0].Text != "attachment 1 text" {
- t.Fatal("should've received correct first attachment text")
- } else if attachments[0].Pretext != "attachment 1 pretext" {
- t.Fatal("should've received correct first attachment pretext")
- } else if attachments[1].Text != "attachment 2 text" {
- t.Fatal("should've received correct second attachment text")
- }
+ for _, testCase := range testCases {
+ testCase := testCase
+ t.Run(testCase.Description, func(t *testing.T) {
+ t.Parallel()
- fields := attachments[1].Fields
- if len(fields) != 2 {
- t.Fatal("should've received 2 fields")
- } else if fields[0].Value.(string) != "value 1" {
- t.Fatal("should've received correct first attachment value")
- } else if _, ok := fields[1].Value.(string); !ok {
- t.Fatal("should've received second attachment value parsed as a string")
- } else if fields[1].Value.(string) != "[]" {
- t.Fatal("should've received correct second attachment value")
- }
-}
-
-func TestCommandResponseNullArrayItems(t *testing.T) {
- payload := `{"attachments":[{"fields":[{"title":"foo","value":"bar","short":true}, null]}, null]}`
- cr := CommandResponseFromJson(strings.NewReader(payload))
- if cr == nil {
- t.Fatal("CommandResponse should not be nil")
- }
- if len(cr.Attachments) != 1 {
- t.Fatalf("expected one attachment")
- }
- if len(cr.Attachments[0].Fields) != 1 {
- t.Fatalf("expected one field")
+ response, err := CommandResponseFromJson(strings.NewReader(testCase.Json))
+ if testCase.ExpectedError != nil {
+ assert.EqualError(t, err, *testCase.ExpectedError)
+ assert.Nil(t, response)
+ } else {
+ assert.NoError(t, err)
+ if assert.NotNil(t, response) {
+ assert.Equal(t, testCase.ExpectedCommandResponse, response)
+ }
+ }
+ })
}
}
diff --git a/utils/config.go b/utils/config.go
index fa436f70d..51b7ea003 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -4,6 +4,7 @@
package utils
import (
+ "bytes"
"encoding/json"
"fmt"
"io"
@@ -23,6 +24,7 @@ import (
"github.com/mattermost/mattermost-server/einterfaces"
"github.com/mattermost/mattermost-server/model"
+ "github.com/mattermost/mattermost-server/utils/jsonutils"
)
const (
@@ -214,9 +216,19 @@ func (w *ConfigWatcher) Close() {
// ReadConfig reads and parses the given configuration.
func ReadConfig(r io.Reader, allowEnvironmentOverrides bool) (*model.Config, map[string]interface{}, error) {
- v := newViper(allowEnvironmentOverrides)
+ // Pre-flight check the syntax of the configuration file to improve error messaging.
+ configData, err := ioutil.ReadAll(r)
+ if err != nil {
+ return nil, nil, err
+ } else {
+ var rawConfig interface{}
+ if err := json.Unmarshal(configData, &rawConfig); err != nil {
+ return nil, nil, jsonutils.HumanizeJsonError(err, configData)
+ }
+ }
- if err := v.ReadConfig(r); err != nil {
+ v := newViper(allowEnvironmentOverrides)
+ if err := v.ReadConfig(bytes.NewReader(configData)); err != nil {
return nil, nil, err
}
diff --git a/utils/config_test.go b/utils/config_test.go
index fbac577ee..11b110367 100644
--- a/utils/config_test.go
+++ b/utils/config_test.go
@@ -4,6 +4,7 @@
package utils
import (
+ "bytes"
"io/ioutil"
"os"
"path/filepath"
@@ -23,6 +24,19 @@ func TestConfig(t *testing.T) {
InitTranslations(cfg.LocalizationSettings)
}
+func TestReadConfig(t *testing.T) {
+ TranslationsPreInit()
+
+ _, _, err := ReadConfig(bytes.NewReader([]byte(``)), false)
+ require.EqualError(t, err, "parsing error at line 1, character 1: unexpected end of JSON input")
+
+ _, _, err = ReadConfig(bytes.NewReader([]byte(`
+ {
+ malformed
+ `)), false)
+ require.EqualError(t, err, "parsing error at line 3, character 5: invalid character 'm' looking for beginning of object key string")
+}
+
func TestTimezoneConfig(t *testing.T) {
TranslationsPreInit()
supportedTimezones := LoadTimezones("timezones.json")
diff --git a/utils/jsonutils/json.go b/utils/jsonutils/json.go
new file mode 100644
index 000000000..da77a2b6b
--- /dev/null
+++ b/utils/jsonutils/json.go
@@ -0,0 +1,56 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package jsonutils
+
+import (
+ "bytes"
+ "encoding/json"
+
+ "github.com/pkg/errors"
+)
+
+type HumanizedJsonError struct {
+ Err error
+ Line int
+ Character int
+}
+
+func (e *HumanizedJsonError) Error() string {
+ return e.Err.Error()
+}
+
+// HumanizeJsonError extracts error offsets and annotates the error with useful context
+func HumanizeJsonError(err error, data []byte) error {
+ if syntaxError, ok := err.(*json.SyntaxError); ok {
+ return NewHumanizedJsonError(syntaxError, data, syntaxError.Offset)
+ } else if unmarshalError, ok := err.(*json.UnmarshalTypeError); ok {
+ return NewHumanizedJsonError(unmarshalError, data, unmarshalError.Offset)
+ } else {
+ return err
+ }
+}
+
+func NewHumanizedJsonError(err error, data []byte, offset int64) *HumanizedJsonError {
+ if err == nil {
+ return nil
+ }
+
+ if offset < 0 || offset > int64(len(data)) {
+ return &HumanizedJsonError{
+ Err: errors.Wrapf(err, "invalid offset %d", offset),
+ }
+ }
+
+ lineSep := []byte{'\n'}
+
+ line := bytes.Count(data[:offset], lineSep) + 1
+ lastLineOffset := bytes.LastIndex(data[:offset], lineSep)
+ character := int(offset) - (lastLineOffset + 1) + 1
+
+ return &HumanizedJsonError{
+ Line: line,
+ Character: character,
+ Err: errors.Wrapf(err, "parsing error at line %d, character %d", line, character),
+ }
+}
diff --git a/utils/jsonutils/json_test.go b/utils/jsonutils/json_test.go
new file mode 100644
index 000000000..b3986e87b
--- /dev/null
+++ b/utils/jsonutils/json_test.go
@@ -0,0 +1,237 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package jsonutils_test
+
+import (
+ "encoding/json"
+ "reflect"
+ "testing"
+
+ "github.com/pkg/errors"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/mattermost/mattermost-server/utils/jsonutils"
+)
+
+func TestHumanizeJsonError(t *testing.T) {
+ t.Parallel()
+
+ type testType struct{}
+
+ testCases := []struct {
+ Description string
+ Data []byte
+ Err error
+ ExpectedErr string
+ }{
+ {
+ "nil error",
+ []byte{},
+ nil,
+ "",
+ },
+ {
+ "non-special error",
+ []byte{},
+ errors.New("test"),
+ "test",
+ },
+ {
+ "syntax error, offset 17, middle of line 3",
+ []byte("line 1\nline 2\nline 3"),
+ &json.SyntaxError{
+ // msg can't be set
+ Offset: 17,
+ },
+ "parsing error at line 3, character 4: ",
+ },
+ {
+ "unmarshal type error, offset 17, middle of line 3",
+ []byte("line 1\nline 2\nline 3"),
+ &json.UnmarshalTypeError{
+ Value: "bool",
+ Type: reflect.TypeOf(testType{}),
+ Offset: 17,
+ Struct: "struct",
+ Field: "field",
+ },
+ "parsing error at line 3, character 4: json: cannot unmarshal bool into Go struct field struct.field of type jsonutils_test.testType",
+ },
+ }
+
+ for _, testCase := range testCases {
+ testCase := testCase
+ t.Run(testCase.Description, func(t *testing.T) {
+ actual := jsonutils.HumanizeJsonError(testCase.Err, testCase.Data)
+ if testCase.ExpectedErr == "" {
+ assert.NoError(t, actual)
+ } else {
+ assert.EqualError(t, actual, testCase.ExpectedErr)
+ }
+ })
+ }
+}
+
+func TestNewHumanizedJsonError(t *testing.T) {
+ t.Parallel()
+
+ type testType struct{}
+
+ testCases := []struct {
+ Description string
+ Data []byte
+ Offset int64
+ Err error
+ Expected *jsonutils.HumanizedJsonError
+ }{
+ {
+ "nil error",
+ []byte{},
+ 0,
+ nil,
+ nil,
+ },
+ {
+ "offset -1, before start of string",
+ []byte("line 1\nline 2\nline 3"),
+ -1,
+ errors.New("message"),
+ &jsonutils.HumanizedJsonError{
+ Err: errors.Wrap(errors.New("message"), "invalid offset -1"),
+ },
+ },
+ {
+ "offset 0, start of string",
+ []byte("line 1\nline 2\nline 3"),
+ 0,
+ errors.New("message"),
+ &jsonutils.HumanizedJsonError{
+ Err: errors.Wrap(errors.New("message"), "parsing error at line 1, character 1"),
+ Line: 1,
+ Character: 1,
+ },
+ },
+ {
+ "offset 5, end of line 1",
+ []byte("line 1\nline 2\nline 3"),
+ 5,
+ errors.New("message"),
+ &jsonutils.HumanizedJsonError{
+ Err: errors.Wrap(errors.New("message"), "parsing error at line 1, character 6"),
+ Line: 1,
+ Character: 6,
+ },
+ },
+ {
+ "offset 6, new line at end end of line 1",
+ []byte("line 1\nline 2\nline 3"),
+ 6,
+ errors.New("message"),
+ &jsonutils.HumanizedJsonError{
+ Err: errors.Wrap(errors.New("message"), "parsing error at line 1, character 7"),
+ Line: 1,
+ Character: 7,
+ },
+ },
+ {
+ "offset 7, start of line 2",
+ []byte("line 1\nline 2\nline 3"),
+ 7,
+ errors.New("message"),
+ &jsonutils.HumanizedJsonError{
+ Err: errors.Wrap(errors.New("message"), "parsing error at line 2, character 1"),
+ Line: 2,
+ Character: 1,
+ },
+ },
+ {
+ "offset 12, end of line 2",
+ []byte("line 1\nline 2\nline 3"),
+ 12,
+ errors.New("message"),
+ &jsonutils.HumanizedJsonError{
+ Err: errors.Wrap(errors.New("message"), "parsing error at line 2, character 6"),
+ Line: 2,
+ Character: 6,
+ },
+ },
+ {
+ "offset 13, newline at end of line 2",
+ []byte("line 1\nline 2\nline 3"),
+ 13,
+ errors.New("message"),
+ &jsonutils.HumanizedJsonError{
+ Err: errors.Wrap(errors.New("message"), "parsing error at line 2, character 7"),
+ Line: 2,
+ Character: 7,
+ },
+ },
+ {
+ "offset 17, middle of line 3",
+ []byte("line 1\nline 2\nline 3"),
+ 17,
+ errors.New("message"),
+ &jsonutils.HumanizedJsonError{
+ Err: errors.Wrap(errors.New("message"), "parsing error at line 3, character 4"),
+ Line: 3,
+ Character: 4,
+ },
+ },
+ {
+ "offset 19, end of string",
+ []byte("line 1\nline 2\nline 3"),
+ 19,
+ errors.New("message"),
+ &jsonutils.HumanizedJsonError{
+ Err: errors.Wrap(errors.New("message"), "parsing error at line 3, character 6"),
+ Line: 3,
+ Character: 6,
+ },
+ },
+ {
+ "offset 20, offset = length of string",
+ []byte("line 1\nline 2\nline 3"),
+ 20,
+ errors.New("message"),
+ &jsonutils.HumanizedJsonError{
+ Err: errors.Wrap(errors.New("message"), "parsing error at line 3, character 7"),
+ Line: 3,
+ Character: 7,
+ },
+ },
+ {
+ "offset 21, offset = length of string, after newline",
+ []byte("line 1\nline 2\nline 3\n"),
+ 21,
+ errors.New("message"),
+ &jsonutils.HumanizedJsonError{
+ Err: errors.Wrap(errors.New("message"), "parsing error at line 4, character 1"),
+ Line: 4,
+ Character: 1,
+ },
+ },
+ {
+ "offset 21, offset > length of string",
+ []byte("line 1\nline 2\nline 3"),
+ 21,
+ errors.New("message"),
+ &jsonutils.HumanizedJsonError{
+ Err: errors.Wrap(errors.New("message"), "invalid offset 21"),
+ },
+ },
+ }
+
+ for _, testCase := range testCases {
+ testCase := testCase
+ t.Run(testCase.Description, func(t *testing.T) {
+ actual := jsonutils.NewHumanizedJsonError(testCase.Err, testCase.Data, testCase.Offset)
+ if testCase.Expected != nil && actual.Err != nil {
+ if assert.EqualValues(t, testCase.Expected.Err.Error(), actual.Err.Error()) {
+ actual.Err = testCase.Expected.Err
+ }
+ }
+ assert.Equal(t, testCase.Expected, actual)
+ })
+ }
+}