From 9092336bf98653131ca6acd2dd58d3210300609f Mon Sep 17 00:00:00 2001 From: Erik Winter Date: Sat, 30 Jan 2021 11:20:12 +0100 Subject: [PATCH] some date and repo tests --- internal/task/date.go | 58 ++++++++++- internal/task/date_test.go | 36 +++++++ internal/task/repo.go | 19 ++-- internal/task/repo_test.go | 206 +++++++++++++++++++++++++++++++++++++ internal/task/task.go | 42 +++++++- internal/task/task_test.go | 37 +++++-- pkg/mstore/email.go | 2 +- pkg/mstore/memory.go | 49 ++++----- pkg/mstore/memory_test.go | 123 ++++++++++------------ pkg/mstore/mstore.go | 4 +- 10 files changed, 446 insertions(+), 130 deletions(-) create mode 100644 internal/task/date_test.go create mode 100644 internal/task/repo_test.go diff --git a/internal/task/date.go b/internal/task/date.go index 298435a..a0198f2 100644 --- a/internal/task/date.go +++ b/internal/task/date.go @@ -1,9 +1,57 @@ package task -import "time" +import ( + "time" +) -type Date time.Time - -func (d *Date) Weekday() Weekday { - return d.Weekday() +type Date struct { + t time.Time +} + +func NewDate(year, month, day int) *Date { + var m time.Month + switch month { + case 1: + m = time.January + case 2: + m = time.February + case 3: + m = time.March + case 4: + m = time.April + case 5: + m = time.May + case 6: + m = time.June + case 7: + m = time.July + case 8: + m = time.August + case 9: + m = time.September + case 10: + m = time.October + case 11: + m = time.November + case 12: + m = time.December + } + + t := time.Date(year, m, day, 10, 0, 0, 0, time.UTC) + + if year == 0 && month == 0 && day == 0 { + t = time.Time{} + } + + return &Date{ + t: t, + } +} + +func (d *Date) String() string { + if d.t.IsZero() { + return "no date" + } + + return d.t.Format("2006-01-02") } diff --git a/internal/task/date_test.go b/internal/task/date_test.go new file mode 100644 index 0000000..494ed39 --- /dev/null +++ b/internal/task/date_test.go @@ -0,0 +1,36 @@ +package task_test + +import ( + "testing" + + "git.sr.ht/~ewintr/go-kit/test" + "git.sr.ht/~ewintr/gte/internal/task" +) + +func TestDateString(t *testing.T) { + for _, tc := range []struct { + name string + date *task.Date + exp string + }{ + { + name: "zero", + date: task.NewDate(0, 0, 0), + exp: "no date", + }, + { + name: "normal", + date: task.NewDate(2021, 1, 30), + exp: "2021-01-30", + }, + { + name: "normalize", + date: task.NewDate(2021, 1, 32), + exp: "2021-02-01", + }, + } { + t.Run(tc.name, func(t *testing.T) { + test.Equals(t, tc.exp, tc.date.String()) + }) + } +} diff --git a/internal/task/repo.go b/internal/task/repo.go index 4f973cb..894ebce 100644 --- a/internal/task/repo.go +++ b/internal/task/repo.go @@ -8,7 +8,9 @@ import ( ) var ( - ErrMStoreError = errors.New("mstore gave error response") + ErrMStoreError = errors.New("mstore gave error response") + ErrInvalidTask = errors.New("invalid task") + ErrInvalidMessage = errors.New("task contains invalid message") ) type TaskRepo struct { @@ -24,7 +26,7 @@ func NewRepository(ms mstore.MStorer) *TaskRepo { func (tr *TaskRepo) FindAll(folder string) ([]*Task, error) { msgs, err := tr.mstore.Messages(folder) if err != nil { - return []*Task{}, err + return []*Task{}, fmt.Errorf("%w: %v", ErrMStoreError, err) } tasks := []*Task{} @@ -38,6 +40,9 @@ func (tr *TaskRepo) FindAll(folder string) ([]*Task, error) { } func (tr *TaskRepo) Update(t *Task) error { + if t == nil { + return ErrInvalidTask + } if !t.Current { return ErrOutdatedTask } @@ -82,22 +87,20 @@ func (tr *TaskRepo) CleanUp() error { // determine which ones need to be gone var tobeRemoved []*Task for _, tasks := range taskSet { - maxUid := uint32(0) + maxVersion := 0 for _, t := range tasks { - if t.Message.Uid > maxUid { - maxUid = t.Message.Uid + if t.Version > maxVersion { + maxVersion = t.Version } } for _, t := range tasks { - if t.Message.Uid < maxUid { + if t.Version < maxVersion { tobeRemoved = append(tobeRemoved, t) } } } - //fmt.Printf("removing: %+v\n", tobeRemoved) - // remove them for _, t := range tobeRemoved { if err := tr.mstore.Remove(t.Message); err != nil { diff --git a/internal/task/repo_test.go b/internal/task/repo_test.go new file mode 100644 index 0000000..4e385ed --- /dev/null +++ b/internal/task/repo_test.go @@ -0,0 +1,206 @@ +package task_test + +import ( + "errors" + "fmt" + "testing" + + "git.sr.ht/~ewintr/go-kit/test" + "git.sr.ht/~ewintr/gte/internal/task" + "git.sr.ht/~ewintr/gte/pkg/mstore" +) + +func TestRepoFindAll(t *testing.T) { + folderA := "folderA" + folderB := "folderB" + + type msgs struct { + Folder string + Subject string + } + + for _, tc := range []struct { + name string + tasks []msgs + folder string + expTasks int + expErr error + }{ + { + name: "empty", + folder: folderA, + }, + { + name: "unknown folder", + folder: "unknown", + expErr: task.ErrMStoreError, + }, + { + name: "not empty", + folder: folderA, + tasks: []msgs{ + {Folder: folderA, Subject: "sub-1"}, + {Folder: folderA, Subject: "sub-2"}, + {Folder: folderB, Subject: "sub-3"}, + {Folder: folderA, Subject: "sub-4"}, + }, + expTasks: 3, + }, + } { + t.Run(tc.name, func(t *testing.T) { + store, err := mstore.NewMemory([]string{folderA, folderB}) + test.OK(t, err) + for _, task := range tc.tasks { + test.OK(t, store.Add(task.Folder, task.Subject, "body")) + } + repo := task.NewRepository(store) + actTasks, err := repo.FindAll(tc.folder) + test.Equals(t, true, errors.Is(err, tc.expErr)) + if err != nil { + return + } + test.Equals(t, tc.expTasks, len(actTasks)) + + }) + } +} + +func TestRepoUpdate(t *testing.T) { + id := "id" + oldFolder := "old folder" + folder := "folder" + action := "action" + + oldMsg := &mstore.Message{ + Uid: 1, + Folder: oldFolder, + Subject: "old subject", + Body: "old body", + } + + for _, tc := range []struct { + name string + task *task.Task + expErr error + expMsgs []*mstore.Message + }{ + { + name: "nil task", + expErr: task.ErrInvalidTask, + }, + { + name: "task without message", + task: &task.Task{ + Id: id, + Folder: folder, + Action: action, + Current: true, + Dirty: true, + }, + expErr: task.ErrMStoreError, + }, + { + name: "outdated task", + task: &task.Task{ + Id: id, + Folder: folder, + Action: action, + Dirty: true, + }, + expErr: task.ErrOutdatedTask, + }, + /* + { + name: "unchanged task", + task: &task.Task{ + Id: id, + Folder: folder, + Action: action, + Current: true, + }, + expMsgs: []*mstore.Message{}, + }, + */ + { + name: "changed task", + task: &task.Task{ + Id: id, + Folder: folder, + Action: action, + Current: true, + Dirty: true, + Message: oldMsg, + }, + expMsgs: []*mstore.Message{ + {Uid: 2, Folder: folder, Subject: action}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + mem, err := mstore.NewMemory([]string{folder, oldFolder}) + test.OK(t, err) + test.OK(t, mem.Add(oldMsg.Folder, oldMsg.Subject, oldMsg.Body)) + + repo := task.NewRepository(mem) + + actErr := repo.Update(tc.task) + test.Equals(t, true, errors.Is(actErr, tc.expErr)) + if tc.expErr != nil { + return + } + + actMsgs, err := mem.Messages(folder) + test.OK(t, err) + for i, _ := range actMsgs { + actMsgs[i].Body = "" + } + test.Equals(t, tc.expMsgs, actMsgs) + }) + } +} + +func TestRepoCleanUp(t *testing.T) { + folderNew := "New" + folderPlanned := "Planned" + folders := []string{"INBOX", folderNew, "Recurring", + folderPlanned, "Unplanned", + } + id := "id" + subject := "subject" + + mem, err := mstore.NewMemory(folders) + test.OK(t, err) + + for v := 1; v <= 3; v++ { + body := fmt.Sprintf(` +id: %s +version: %d +`, id, v) + folder := folderNew + if v%2 == 1 { + folder = folderPlanned + } + test.OK(t, mem.Add(folder, subject, body)) + } + + repo := task.NewRepository(mem) + test.OK(t, repo.CleanUp()) + + expNew := []*mstore.Message{} + actNew, err := mem.Messages(folderNew) + test.OK(t, err) + test.Equals(t, expNew, actNew) + expPlanned := []*mstore.Message{{ + Uid: 3, + Folder: folderPlanned, + Subject: subject, + Body: ` +id: id +version: 3 +`, + }} + actPlanned, err := mem.Messages(folderPlanned) + test.OK(t, err) + fmt.Printf("act: %+v\n", actPlanned[0]) + test.Equals(t, expPlanned, actPlanned) +} diff --git a/internal/task/task.go b/internal/task/task.go index 0604a5b..ad16a22 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -3,6 +3,7 @@ package task import ( "errors" "fmt" + "strconv" "strings" "git.sr.ht/~ewintr/gte/pkg/mstore" @@ -14,8 +15,11 @@ var ( ) const ( - FOLDER_INBOX = "INBOX" - FOLDER_NEW = "New" + FOLDER_INBOX = "INBOX" + FOLDER_NEW = "New" + FOLDER_RECURRING = "Recurring" + FOLDER_PLANNED = "Planned" + FOLDER_UNPLANNED = "Unplanned" QUOTE_PREFIX = ">" PREVIOUS_SEPARATOR = "Previous version:" @@ -24,13 +28,20 @@ const ( SUBJECT_SEPARATOR = " - " FIELD_ID = "id" + FIELD_VERSION = "version" FIELD_ACTION = "action" FIELD_PROJECT = "project" - FIELD_DUE = "date" + FIELD_DUE = "due" ) var ( - knownFolders = []string{FOLDER_INBOX, FOLDER_NEW} + knownFolders = []string{ + FOLDER_INBOX, + FOLDER_NEW, + FOLDER_RECURRING, + FOLDER_PLANNED, + FOLDER_UNPLANNED, + } ) // Task reperesents a task based on the data stored in a message @@ -38,6 +49,8 @@ type Task struct { // Id is a UUID that gets carried over when a new message is constructed Id string + // Version is a method to determine the latest version for cleanup + Version int // Folder is the same name as the mstore folder Folder string @@ -74,6 +87,13 @@ func New(msg *mstore.Message) *Task { dirty = true } + // Version, cannot manually be incremented from body + versionStr, _ := FieldFromBody(FIELD_VERSION, msg.Body) + version, _ := strconv.Atoi(versionStr) + if version == 0 { + dirty = true + } + // Action action, d := FieldFromBody(FIELD_ACTION, msg.Body) if action == "" { @@ -99,8 +119,13 @@ func New(msg *mstore.Message) *Task { dirty = true } + if dirty { + version++ + } + return &Task{ Id: id, + Version: version, Folder: folder, Action: action, Project: project, @@ -129,9 +154,10 @@ func (t *Task) FormatSubject() string { func (t *Task) FormatBody() string { body := fmt.Sprintf("\n") - order := []string{FIELD_ID, FIELD_PROJECT, FIELD_ACTION} + order := []string{FIELD_ID, FIELD_VERSION, FIELD_PROJECT, FIELD_ACTION} fields := map[string]string{ FIELD_ID: t.Id, + FIELD_VERSION: strconv.Itoa(t.Version), FIELD_PROJECT: t.Project, FIELD_ACTION: t.Action, } @@ -195,3 +221,9 @@ func FieldFromSubject(field, subject string) string { return "" } + +func increment(s string) string { + i, _ := strconv.Atoi(s) + i++ + return strconv.Itoa(i) +} diff --git a/internal/task/task_test.go b/internal/task/task_test.go index a53af9e..7163abb 100644 --- a/internal/task/task_test.go +++ b/internal/task/task_test.go @@ -11,15 +11,17 @@ import ( func TestNewFromMessage(t *testing.T) { id := "an id" + version := 2 action := "some action" project := "project" folder := task.FOLDER_NEW for _, tc := range []struct { - name string - message *mstore.Message - hasId bool - exp *task.Task + name string + message *mstore.Message + hasId bool + hasVersion bool + exp *task.Task }{ { name: "empty", @@ -34,13 +36,16 @@ func TestNewFromMessage(t *testing.T) { Folder: folder, Body: fmt.Sprintf(` id: %s +version: %d action: %s project: %s -`, id, action, project), +`, id, version, action, project), }, - hasId: true, + hasId: true, + hasVersion: true, exp: &task.Task{ Id: id, + Version: version, Folder: folder, Action: action, Project: project, @@ -70,14 +75,17 @@ action: %s Subject: "some other action", Body: fmt.Sprintf(` id: %s +version: %d action: %s - `, id, action), + `, id, version, action), }, - hasId: true, + hasId: true, + hasVersion: true, exp: &task.Task{ - Id: id, - Folder: folder, - Action: action, + Id: id, + Version: version, + Folder: folder, + Action: action, }, }, { @@ -122,6 +130,9 @@ Forwarded message: test.Equals(t, false, "" == act.Id) tc.exp.Id = act.Id } + if !tc.hasVersion { + tc.exp.Version = 1 + } tc.exp.Message = tc.message tc.exp.Current = true test.Equals(t, tc.exp, act) @@ -166,6 +177,7 @@ func TestFormatSubject(t *testing.T) { func TestFormatBody(t *testing.T) { id := "an id" + version := 6 action := "an action" project := "project" @@ -179,6 +191,7 @@ func TestFormatBody(t *testing.T) { task: &task.Task{}, exp: ` id: +version: 0 project: action: `, @@ -187,6 +200,7 @@ action: name: "filled", task: &task.Task{ Id: id, + Version: version, Action: action, Project: project, Message: &mstore.Message{ @@ -195,6 +209,7 @@ action: }, exp: ` id: an id +version: 6 project: project action: an action diff --git a/pkg/mstore/email.go b/pkg/mstore/email.go index 2ad5bb8..5de8db2 100644 --- a/pkg/mstore/email.go +++ b/pkg/mstore/email.go @@ -187,7 +187,7 @@ Subject: %s } func (es *Imap) Remove(msg *Message) error { - if !msg.Valid() { + if msg == nil || !msg.Valid() { return ErrInvalidMessage } diff --git a/pkg/mstore/memory.go b/pkg/mstore/memory.go index e9c704b..89fbfcd 100644 --- a/pkg/mstore/memory.go +++ b/pkg/mstore/memory.go @@ -11,8 +11,8 @@ var ( ) type Memory struct { - Selected string nextUid uint32 + folders []string messages map[string][]*Message } @@ -20,6 +20,7 @@ func NewMemory(folders []string) (*Memory, error) { if len(folders) == 0 { return &Memory{}, ErrInvalidFolderSet } + sort.Strings(folders) msg := make(map[string][]*Message) for _, f := range folders { @@ -31,29 +32,13 @@ func NewMemory(folders []string) (*Memory, error) { return &Memory{ messages: msg, + folders: folders, nextUid: uint32(1), }, nil } func (mem *Memory) Folders() ([]string, error) { - folders := []string{} - for f := range mem.messages { - folders = append(folders, f) - } - - sort.Strings(folders) - - return folders, nil -} - -func (mem *Memory) Select(folder string) error { - if _, ok := mem.messages[folder]; !ok { - return ErrFolderDoesNotExist - } - - mem.Selected = folder - - return nil + return mem.folders, nil } func (mem *Memory) Add(folder, subject, body string) error { @@ -66,6 +51,7 @@ func (mem *Memory) Add(folder, subject, body string) error { mem.messages[folder] = append(mem.messages[folder], &Message{ Uid: mem.nextUid, + Folder: folder, Subject: subject, Body: body, }) @@ -74,28 +60,29 @@ func (mem *Memory) Add(folder, subject, body string) error { return nil } -func (mem *Memory) Messages() ([]*Message, error) { - if mem.Selected == "" { - return []*Message{}, ErrNoFolderSelected +func (mem *Memory) Messages(folder string) ([]*Message, error) { + if _, ok := mem.messages[folder]; !ok { + return []*Message{}, ErrFolderDoesNotExist } - return mem.messages[mem.Selected], nil + return mem.messages[folder], nil } -func (mem *Memory) Remove(uid uint32) error { - if uid == uint32(0) { - return ErrInvalidUid +func (mem *Memory) Remove(msg *Message) error { + if msg == nil || !msg.Valid() { + return ErrInvalidMessage } - if mem.Selected == "" { - return ErrNoFolderSelected + if _, ok := mem.messages[msg.Folder]; !ok { + return ErrFolderDoesNotExist } - for i, m := range mem.messages[mem.Selected] { - if m.Uid == uid { - mem.messages[mem.Selected] = append(mem.messages[mem.Selected][:i], mem.messages[mem.Selected][i+1:]...) + for i, m := range mem.messages[msg.Folder] { + if m.Uid == msg.Uid { + mem.messages[msg.Folder] = append(mem.messages[msg.Folder][:i], mem.messages[msg.Folder][i+1:]...) return nil } } + return ErrMessageDoesNotExist } diff --git a/pkg/mstore/memory_test.go b/pkg/mstore/memory_test.go index a98609a..141b2e7 100644 --- a/pkg/mstore/memory_test.go +++ b/pkg/mstore/memory_test.go @@ -67,58 +67,41 @@ func TestMemoryFolders(t *testing.T) { } } -func TestMemorySelect(t *testing.T) { - mem, err := mstore.NewMemory([]string{"one", "two", "three"}) - test.OK(t, err) - for _, tc := range []struct { - name string - folder string - exp error - }{ - { - name: "empty", - exp: mstore.ErrFolderDoesNotExist, - }, - { - name: "not present", - folder: "four", - exp: mstore.ErrFolderDoesNotExist, - }, - { - name: "present", - folder: "two", - }, - } { - t.Run(tc.name, func(t *testing.T) { - test.Equals(t, tc.exp, mem.Select(tc.folder)) - if tc.exp == nil { - test.Equals(t, tc.folder, mem.Selected) - } - }) - } -} - func TestMemoryAdd(t *testing.T) { folder := "folder" + subject := "subject" for _, tc := range []struct { name string + folder string subject string - exp error + expMsgs []*mstore.Message + expErr error }{ { - name: "empty", - exp: mstore.ErrInvalidMessage, + name: "empty", + folder: folder, + expErr: mstore.ErrInvalidMessage, + }, + { + name: "invalid folder", + folder: "not there", + subject: subject, + expErr: mstore.ErrFolderDoesNotExist, }, { name: "valid", - subject: "subject", + folder: folder, + subject: subject, + expMsgs: []*mstore.Message{ + {Uid: 1, Folder: folder, Subject: subject}, + }, }, } { t.Run(tc.name, func(t *testing.T) { mem, err := mstore.NewMemory([]string{folder}) test.OK(t, err) - test.Equals(t, tc.exp, mem.Add(folder, tc.subject, "")) + test.Equals(t, tc.expErr, mem.Add(tc.folder, tc.subject, "")) }) } } @@ -127,19 +110,17 @@ func TestMemoryMessages(t *testing.T) { folderA := "folderA" folderB := "folderB" - t.Run("no folder selected", func(t *testing.T) { - mem, err := mstore.NewMemory([]string{folderA}) - test.OK(t, err) - _, err = mem.Messages() - test.Equals(t, mstore.ErrNoFolderSelected, err) - }) - for _, tc := range []struct { name string folder string amount int expErr error }{ + { + name: "unknown folder", + folder: "not there", + expErr: mstore.ErrFolderDoesNotExist, + }, { name: "empty folder", folder: folderB, @@ -164,6 +145,7 @@ func TestMemoryMessages(t *testing.T) { for i := 1; i <= tc.amount; i++ { m := &mstore.Message{ Uid: uint32(i), + Folder: folderA, Subject: fmt.Sprintf("subject-%d", i), Body: fmt.Sprintf("body-%d", i), } @@ -173,8 +155,7 @@ func TestMemoryMessages(t *testing.T) { test.OK(t, mem.Add(folderA, m.Subject, m.Body)) } - test.OK(t, mem.Select(tc.folder)) - actMessages, err := mem.Messages() + actMessages, err := mem.Messages(tc.folder) test.Equals(t, tc.expErr, err) test.Equals(t, expMessages, actMessages) }) @@ -183,12 +164,7 @@ func TestMemoryMessages(t *testing.T) { func TestMemoryRemove(t *testing.T) { folderA, folderB := "folderA", "folderB" - - t.Run("no folder selected", func(t *testing.T) { - mem, err := mstore.NewMemory([]string{folderA}) - test.OK(t, err) - test.Equals(t, mstore.ErrNoFolderSelected, mem.Remove(uint32(3))) - }) + subject := "subject" mem, err := mstore.NewMemory([]string{folderA, folderB}) test.OK(t, err) @@ -197,38 +173,49 @@ func TestMemoryRemove(t *testing.T) { } for _, tc := range []struct { name string - folder string - uid uint32 + msg *mstore.Message expUids []uint32 expErr error }{ { - name: "invalid uid", - folder: folderA, - uid: uint32(0), - expErr: mstore.ErrInvalidUid, - }, - { - name: "empty", - folder: folderB, - uid: uint32(1), + name: "empty", + msg: &mstore.Message{ + Uid: 1, + Folder: folderB, + Subject: subject, + }, expErr: mstore.ErrMessageDoesNotExist, }, { - name: "valid", - folder: folderA, - uid: uint32(2), + name: "nil message", + expErr: mstore.ErrInvalidMessage, + }, + { + name: "unknown folder", + msg: &mstore.Message{ + Uid: 1, + Folder: "unknown", + Subject: subject, + }, + expErr: mstore.ErrFolderDoesNotExist, + }, + { + name: "valid", + msg: &mstore.Message{ + Uid: 2, + Folder: folderA, + Subject: subject, + }, expUids: []uint32{1, 3}, }, } { t.Run(tc.name, func(t *testing.T) { - test.OK(t, mem.Select(tc.folder)) - test.Equals(t, tc.expErr, mem.Remove(tc.uid)) + test.Equals(t, tc.expErr, mem.Remove(tc.msg)) if tc.expErr != nil { return } actUids := []uint32{} - actMsgs, err := mem.Messages() + actMsgs, err := mem.Messages(tc.msg.Folder) test.OK(t, err) for _, m := range actMsgs { actUids = append(actUids, m.Uid) diff --git a/pkg/mstore/mstore.go b/pkg/mstore/mstore.go index d9ecab5..51e82a1 100644 --- a/pkg/mstore/mstore.go +++ b/pkg/mstore/mstore.go @@ -1,6 +1,8 @@ package mstore -import "errors" +import ( + "errors" +) var ( ErrFolderDoesNotExist = errors.New("folder does not exist")