package models import ( "bufio" "encoding" "encoding/json" "regexp" "strings" "testing" "time" "github.com/nalgeon/be" ) // 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", []string{"@id .+ @end"}, nil}, { "one-field", "Title 2", when, []Meta{{"age", 41}}, "@begin " + whens + " - Title 2", []string{"@age 41", "@id .*"}, 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, }, { "nested-json", "Title N", when, []Meta{{"me", json.RawMessage(`{"age": 43, "cool": true}`)}}, "@begin " + whens + " - Title N", []string{"@me:age 43", "@me:cool true"}, nil, }, { "nested-map", "Title M", when, []Meta{{"me", map[string]any{"age": 43, "cool": true}}}, "@begin " + whens + " - Title M", []string{"@me:age 43", "@me:cool true"}, nil, }, { "double-nested-map", "Title DM", when, []Meta{{"me", map[string]any{"age": 43, "name": map[string]any{"first": "Dan", "last": "Jones"}}}}, "@begin " + whens + " - Title DM", []string{"@me:age 43", "@me:name:first Dan", "@me:name:last Jones"}, nil, }, { "double-nested-map-dot", "Title DM", when, []Meta{{"me", map[string]any{"age": 43, "name": map[string]any{".": "Dan Jones", "nick": "Danny"}}}}, "@begin " + whens + " - Title DM", []string{"@me:age 43", "@me:name Dan Jones", "@me:name:nick Danny"}, nil, }, { "double-nested-map-blank", "Title DM", when, []Meta{{"me", map[string]any{"age": 43, "name": map[string]any{"": "Dan Jones", "nick": "Danny"}}}}, "@begin " + whens + " - Title DM", []string{"@me:age 43", "@me:name Dan Jones", "@me:name:nick Danny"}, nil, }, { "nested-keys-in-json", "Title NKJ", when, []Meta{{"me", json.RawMessage(`{"name:first": "Dan", "name:last": "Jones"}`)}}, "@begin " + whens + " - Title NKJ", []string{"@me:name:first Dan", "@me:name:last Jones"}, 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() be.Err(t, er, err) if first == "" { return } os := string(o) if len(lines) == 0 { be.Equal(t, os, first) return } be.True(t, regexp.MustCompile(first).MatchString(os)) for _, line := range lines { be.True(t, regexp.MustCompile("(?m)^"+line).MatchString(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, }, { "nested-field", "@begin " + whens + " - A Title\n@me:name Dan\n@me:coder true @end", "A Title", when, []Meta{{"me:name", "Dan"}, {"me:coder", true}}, nil, }, { "nested-field-dot", "@begin " + whens + " - A Title\n@me:name Dan Jones\n@me:name:nick Danny\n@me:coder true @end", "A Title", when, []Meta{{"me:name", "Dan Jones"}, {"me:name:nick", "Danny"}, {"me: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 { be.Err(t, er, err) return } be.Equal(t, e.Title, title) be.True(t, e.Date.After(date.Add(-time.Second)) && e.Date.Before(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 } } be.True(t, got) } } } 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) be.True(t, scan.Scan()) be.Equal(t, scan.Text(), "@begin date - Title\nlong") be.True(t, scan.Scan()) be.Equal(t, scan.Text(), "@foo john\njones") be.True(t, scan.Scan()) be.Equal(t, scan.Text(), "@bar 42@nobody") be.True(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}, {"nested-field", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:title", "Sub-title"}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","title":"Sub-title"}}`, nil}, {"double-nested-field", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:me:name", "Dan"}, {"obj:me:age", 27}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","me":{"name":"Dan","age":27}}}`, nil}, {"nested-plus-json", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:me", json.RawMessage(`{"name":"Dan","age":27}`)}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","me":{"name":"Dan","age":27}}}`, nil}, {"nested-part", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:me", "Dan"}, {"obj:me:age", 27}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","me":{".":"Dan","age":27}}}`, nil}, {"nested-part-order", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:me:age", 27}, {"obj:me", "Dan"}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","me":{".":"Dan","age":27}}}`, nil}, {"nested-part-order-two", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:me:age", 27}, {"obj:me", "Dan"}, {"obj:me:cool", true}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","me":{".":"Dan","age":27,"cool":true}}}`, 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 { var got, want any json.Unmarshal([]byte(out), &want) json.Unmarshal(o, &got) be.Equal(t, got, want) } else { be.Err(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 { be.Err(t, er, err) return } be.Equal(t, er, nil) be.Equal(t, e.Title, title) be.True(t, e.Date.After(date.Add(-time.Second)) && e.Date.Before(date.Add(time.Second))) be.Equal(t, len(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 mTime.After(fTime.Add(-2*time.Second)) && mTime.Before(fTime.Add(2*time.Second)) { got = true break } } } be.True(t, got) } } }