package models import ( "bufio" "encoding" "encoding/json" "strings" "testing" "time" "github.com/stretchr/testify/assert" ) // Type assertions var _ encoding.TextMarshaler = Entry{} var _ encoding.TextUnmarshaler = new(Entry) var _ json.Marshaler = Entry{} var _ json.Unmarshaler = new(Entry) func TestEntryMarshal(t *testing.T) { when := time.Now() whens := when.Format(DateFormat) simple := []Meta{} nolines := []string{} tests := []struct { name string title string date time.Time fields []Meta first string lines []string err error }{ {"no-title", "", when, simple, "", nolines, ErrorMissingTitle}, {"zero-date", "Empty title", time.Time{}, simple, "", nolines, ErrorMissingDate}, {"one-line", "A Title", when, simple, "@begin " + whens + " - A Title @end", nolines, nil}, { "one-field", "Title 2", when, []Meta{{"age", 41}}, "@begin " + whens + " - Title 2", []string{"@age 41 @end"}, nil, }, { "three-fields", "Title 3", when, []Meta{{"age", 41}, {"cool", true}, {"name", "Jim"}}, "@begin " + whens + " - Title 3", []string{"@age 41", "@cool true", "@name Jim"}, nil, }, { "json-field", "Title J", when, []Meta{{"json", json.RawMessage(`{"age": 41, "cool": true, "name": "Jim"}`)}}, "@begin " + whens + " - Title J", []string{"@age 41", "@cool true", "@name Jim"}, nil, }, } for _, tt := range tests { t.Run(tt.name, getEntryMarshalTestRunner(tt.title, tt.date, tt.fields, tt.first, tt.lines, tt.err)) } } func getEntryMarshalTestRunner(title string, date time.Time, fields []Meta, first string, lines []string, err error) func(*testing.T) { return func(t *testing.T) { en := Entry{title, date, fields} o, er := en.MarshalText() assert.Equal(t, err, er) if first == "" { return } os := string(o) if len(lines) == 0 { assert.Equal(t, first, os) return } assert.Regexp(t, first, os) for _, line := range lines { assert.Regexp(t, "(?m)^"+line, os) } } } func TestEntryUnmarshal(t *testing.T) { when := time.Now() whens := when.Format(DateFormat) simple := []Meta{} tests := []struct { name string in string title string date time.Time fields []Meta err error }{ {"one-line", "@begin " + whens + " - A Title @end", "A Title", when, simple, nil}, {"rfc3999-date", "@begin " + when.Format(time.RFC3339) + " - A Title @end", "A Title", when, simple, nil}, {"multi-title", "@begin " + whens + " - A Title\nwith break @end", "A Title\nwith break", when, simple, nil}, {"no-title", "@begin " + whens + " - @end", "", when, simple, ErrorMissingTitle}, {"parse-error", "this is no good", "", when, simple, ErrorParsing}, {"no-date", "@begin - A Title @end", "A Title", when, simple, ErrorMissingDate}, {"bad-date", "@begin not-a-real date - A Title @end", "A Title", when, simple, ErrorParsing}, {"one-field", "@begin " + whens + " - A Title\n@age 41 @end", "A Title", when, []Meta{{"age", 41}}, nil}, { "two-fields", "@begin " + whens + " - A Title\n@age 41\n@cool true @end", "A Title", when, []Meta{{"age", 41}, {"cool", true}}, nil, }, { "obj-field", "@begin " + whens + " - A Title\n" + `@me {"name":"Dan","coder":true} @end`, "A Title", when, []Meta{{"me", json.RawMessage(`{"name":"Dan","coder":true}`)}}, nil, }, { "json-field", "@begin " + whens + " - Some Guy\n" + `@json {"name":"Dan","coder":true} @end`, "Some Guy", when, []Meta{{"name", "Dan"}, {"coder", true}}, nil, }, } for _, tt := range tests { t.Run(tt.name, getEntryUnmarshalTestRunner(tt.in, tt.title, tt.date, tt.fields, tt.err)) } } func getEntryUnmarshalTestRunner(in string, title string, date time.Time, fields []Meta, err error) func(*testing.T) { return func(t *testing.T) { e := &Entry{} er := e.UnmarshalText([]byte(in)) if err != nil { assert.ErrorIs(t, er, err) return } assert.Equal(t, title, e.Title) assert.WithinRange(t, e.Date, date.Add(-time.Second), date.Add(time.Second)) for _, f := range fields { got := false for _, m := range e.Fields { var mVal any = m.Value var fVal any = f.Value if mJ, ok := m.Value.(json.RawMessage); ok { mVal = string(mJ) } if fJ, ok := f.Value.(json.RawMessage); ok { fVal = string(fJ) } if m.Key == f.Key && mVal == fVal { got = true break } } assert.Truef(t, got, "Couldn't find field %+v. We have %+v", f, e.Fields) } } } func TestScan(t *testing.T) { in := "@begin date - Title\nlong\n@foo john\njones\n@bar 42@nobody @end" read := strings.NewReader(in) scan := bufio.NewScanner(read) scan.Split(scanEntry) assert.True(t, scan.Scan()) assert.Equal(t, "@begin date - Title\nlong", scan.Text()) assert.True(t, scan.Scan()) assert.Equal(t, "@foo john\njones", scan.Text()) assert.True(t, scan.Scan()) assert.Equal(t, "@bar 42@nobody", scan.Text()) assert.False(t, scan.Scan()) } func TestEntryJsonMarshal(t *testing.T) { when := time.Now() whens := when.Format(time.RFC3339) simple := []Meta{} tests := []struct { name string title string date time.Time fields []Meta out string err error }{ {"simple", "A Title", when, simple, `{"title":"A Title","date":"` + whens + `"}`, nil}, {"one-field", "A Title 2", when, []Meta{{"age", 41}}, `{"title":"A Title 2","date":"` + whens + `","age":41}`, nil}, {"skip-title-field", "A Title", when, []Meta{{"title", "Different title"}}, `{"title":"A Title","date":"` + whens + `"}`, nil}, {"skip-date-field", "A Title", when, []Meta{{"date", when.Add(time.Hour)}}, `{"title":"A Title","date":"` + whens + `"}`, nil}, {"skip-dupe-field", "A Title", when, []Meta{{"foo", "bar"}, {"foo", "baz"}}, `{"title":"A Title","date":"` + whens + `","foo": "bar"}`, nil}, {"two-fields", "A Title", when, []Meta{{"foo", "bar"}, {"baz", 42}}, `{"title":"A Title","date":"` + whens + `","foo": "bar","baz":42}`, nil}, {"empty-title", "", when, simple, "", ErrorMissingTitle}, {"empty-date", "A Title", time.Time{}, simple, "", ErrorMissingDate}, {"obj-field", "A Title", when, []Meta{{"obj", json.RawMessage(`{"foo":"bar","title":"Sub-title"}`)}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","title":"Sub-title"}}`, nil}, {"date-field", "A Title", when, []Meta{{"when", when.Add(time.Hour)}}, `{"title":"A Title","date":"` + whens + `","when":"` + when.Add(time.Hour).Format(time.RFC3339) + `"}`, nil}, {"json-field", "A Title", when, []Meta{{"json", json.RawMessage(`{"age": 41, "cool": true, "name": "Jim"}`)}}, `{"title":"A Title","date":"` + whens + `","age":41,"cool": true, "name": "Jim"}`, nil}, } for _, tt := range tests { t.Run(tt.name, getEntryJsonMarshalTestRunner(tt.title, tt.date, tt.fields, tt.out, tt.err)) } } func getEntryJsonMarshalTestRunner(title string, date time.Time, fields []Meta, out string, err error) func(t *testing.T) { return func(t *testing.T) { e := Entry{title, date, fields} o, er := json.Marshal(e) if err == nil { assert.JSONEq(t, out, string(o)) } else { assert.ErrorIs(t, er, err) } } } func TestEntryJsonUnmarshal(t *testing.T) { when := time.Now().Truncate(time.Second) whens := when.Format(time.RFC3339) simple := []Meta{} tests := []struct { name string in string title string date time.Time fields []Meta err error }{ {"simple", `{"title":"A Title","date":"` + whens + `"}`, "A Title", when, simple, nil}, {"missing-title", `{"date":"` + whens + `"}`, "", when, simple, ErrorMissingTitle}, {"missing-date", `{"title":"A Title"}`, "", when, simple, ErrorMissingDate}, {"empty-title", `{"title":"","date":"` + whens + `"}`, "", when, simple, ErrorMissingTitle}, {"empty-date", `{"title":"A Title","date":""}`, "", when, simple, ErrorMissingDate}, {"bad-date", `{"title":"A Title","date":"bad"}`, "", when, simple, ErrorParsing}, {"bad-json", `{"title":"A Title","date":"`, "", when, simple, ErrorParsing}, { "single-field", `{"title":"A Title","date":"` + whens + `","hello":"Hi"}`, "A Title", when, []Meta{{"hello", "Hi"}}, nil, }, { "many-fields", `{"title":"A Title","date":"` + whens + `","hello":"Hi","bye":42,"b":true,"fl":42.13}`, "A Title", when, []Meta{{"hello", "Hi"}, {"bye", int64(42)}, {"b", true}, {"fl", float64(42.13)}}, nil, }, { "date-field", `{"title":"A Title","date":"` + whens + `","posted":"` + when.Add(-time.Hour).In(time.UTC).Format(time.RFC3339) + `"}`, "A Title", when, []Meta{{"posted", when.Add(-time.Hour)}}, nil, }, { "json-field", `{"title":"A Title","date":"` + whens + `","json":{"age": 41, "cool": true, "name": "Jim"}}`, "A Title", when, []Meta{{"age", int64(41)}, {"cool", true}, {"name", "Jim"}}, nil, }, { "json-field-embed", `{"title":"A Title","date":"` + whens + `","json":"{\"age\": 41, \"cool\": true, \"name\": \"Jim\"}"}`, "A Title", when, []Meta{{"age", int64(41)}, {"cool", true}, {"name", "Jim"}}, nil, }, } for _, tt := range tests { t.Run(tt.name, getEntryJsonUnmarshalTestRunner(tt.in, tt.title, tt.date, tt.fields, tt.err)) } } func getEntryJsonUnmarshalTestRunner(in, title string, date time.Time, fields []Meta, err error) func(t *testing.T) { return func(t *testing.T) { e := new(Entry) er := e.UnmarshalJSON([]byte(in)) if err != nil { assert.ErrorIs(t, er, err) return } assert.Nil(t, er) assert.Equal(t, title, e.Title) assert.WithinRange(t, e.Date, date.Add(-time.Second), date.Add(time.Second)) assert.Len(t, e.Fields, len(fields)) for _, f := range fields { got := false fTime, isTime := f.Value.(time.Time) for _, m := range e.Fields { var mVal any = m.Value var fVal any = f.Value if mJ, ok := m.Value.(json.RawMessage); ok { mVal = string(mJ) } if fJ, ok := f.Value.(json.RawMessage); ok { fVal = string(fJ) } if m.Key == f.Key && mVal == fVal { got = true break } if isTime && m.Key == f.Key { mTime, _ := mVal.(time.Time) if assert.WithinRange(t, mTime, fTime.Add(-2*time.Second), fTime.Add(2*time.Second)) { got = true break } } } assert.Truef(t, got, "Couldn't find field %+v. We have %+v", f, e.Fields) } } }