summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Hallam <jesse.hallam@gmail.com>2018-04-26 11:19:25 -0400
committerGitHub <noreply@github.com>2018-04-26 11:19:25 -0400
commit6d50d836f538253e2d13d5ddb90495820f9cb259 (patch)
tree2d61042647dec793be5f6e4e87f095dfef658f14
parentd3f09b54e2be65981f0496938f9d5f97507874e6 (diff)
downloadchat-6d50d836f538253e2d13d5ddb90495820f9cb259.tar.gz
chat-6d50d836f538253e2d13d5ddb90495820f9cb259.tar.bz2
chat-6d50d836f538253e2d13d5ddb90495820f9cb259.zip
MM-10232, MM-10259: Improve error handling from invalid json (#8668)
* MM-10232: improve error handling from malformed slash command responses Switch to json.Unmarshal, which doesn't obscure JSON parse failures like json.Decode. The latter is primarily designed for streams of JSON, not necessarily unmarshalling just a single object. * rework HumanizedJsonError to expose Line and Character discretely * MM-10259: pinpoint line and character where json config error occurs * tweak HumanizeJsonError to accept err first
-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)
+ })
+ }
+}