Unittesting begins

This commit is contained in:
Bel LaPointe
2019-11-12 13:45:32 -07:00
commit 8c4bc81694
35 changed files with 3231 additions and 0 deletions

73
server/ajax/ajax.go Normal file
View File

@@ -0,0 +1,73 @@
package ajax
import (
"local/storage"
"local/todo-server/config"
"net/http"
)
type Ajax struct {
DB storage.DB
}
func New() (*Ajax, error) {
db, err := storage.New(storage.TypeFromString(config.StoreType), config.StoreAddr, config.StoreUser, config.StorePass)
return &Ajax{
DB: db,
}, err
}
func (a *Ajax) HandleAjax(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
var foo func(http.ResponseWriter, *http.Request) error
if v := params.Get("loadLists"); v != "" {
foo = a.loadLists
} else if v := params.Get("loadTasks"); v != "" {
foo = a.loadTasks
} else if v := params.Get("newTask"); v != "" {
foo = a.newTask
} else if v := params.Get("fullNewTask"); v != "" {
foo = a.newTask
} else if v := params.Get("deleteTask"); v != "" {
foo = a.deleteTask
} else if v := params.Get("completeTask"); v != "" {
foo = a.completeTask
} else if v := params.Get("editNote"); v != "" {
foo = a.editNote
} else if v := params.Get("editTask"); v != "" {
foo = a.editTask
} else if v := params.Get("changeOrder"); v != "" {
foo = a.changeOrder
} else if v := params.Get("suggestTags"); v != "" {
foo = a.suggestTags
} else if v := params.Get("setPrio"); v != "" {
foo = a.setPrio
} else if v := params.Get("tagCloud"); v != "" {
foo = a.tagCloud
} else if v := params.Get("addList"); v != "" {
foo = a.addList
} else if v := params.Get("renameList"); v != "" {
foo = a.renameList
} else if v := params.Get("deleteList"); v != "" {
foo = a.deleteList
} else if v := params.Get("setSort"); v != "" {
foo = a.setSort
} else if v := params.Get("publishList"); v != "" {
foo = a.publishList
} else if v := params.Get("moveTask"); v != "" {
foo = a.moveTask
} else if v := params.Get("changeListOrder"); v != "" {
foo = a.changeListOrder
} else if v := params.Get("parseTaskStr"); v != "" {
foo = a.parseTaskStr
} else if v := params.Get("clearCompletedInList"); v != "" {
foo = a.clearCompletedInList
} else if v := params.Get("setShowNotesInList"); v != "" {
foo = a.setShowNotesInList
} else if v := params.Get("setHideList"); v != "" {
foo = a.setHideList
}
if err := foo(w, r); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

20
server/ajax/ajax_test.go Normal file
View File

@@ -0,0 +1,20 @@
package ajax
import (
"local/todo-server/config"
"testing"
)
func TestNew(t *testing.T) {
mockAjax()
}
func mockAjax() *Ajax {
config.StoreType = "map"
ajax, err := New()
if err != nil {
panic(err)
}
ajax.storageSetCur("list")
return ajax
}

60
server/ajax/form/form.go Normal file
View File

@@ -0,0 +1,60 @@
package form
import (
"bytes"
"encoding/json"
"html"
"io"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
)
type readCloser struct {
io.Reader
}
func (rc readCloser) Close() error {
return nil
}
func Get(r *http.Request, k string) string {
s := r.FormValue(k)
if s == "" {
b, _ := ioutil.ReadAll(r.Body)
var m map[string]json.RawMessage
if err := json.Unmarshal(b, &m); err != nil {
return ""
}
v, _ := m[k]
s = strings.TrimPrefix(strings.TrimSuffix(string(v), `"`), `"`)
r.Body = readCloser{bytes.NewReader(b)}
}
s = html.UnescapeString(s)
s = strings.ReplaceAll(s, "\r", "")
return s
}
func ToInt(s string) int {
v, _ := strconv.Atoi(s)
return v
}
func ToStrArr(k string) []string {
arr := strings.Split(k, ",")
outArr := []string{}
for i := range arr {
s := strings.TrimSpace(arr[i])
if len(s) > 0 {
outArr = append(outArr, s)
}
}
return outArr
}
func ToTime(s string) time.Time {
v, _ := time.Parse("2006-01-02 15:04:05", s)
return v
}

View File

@@ -0,0 +1,132 @@
package form
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestGet(t *testing.T) {
r := testReq()
if v := Get(r, "a"); v != "b" {
t.Error(v)
}
if v := Get(r, "c"); v != "4" {
t.Error(v)
}
if v := Get(r, "d"); v != "e, f,g" {
t.Error(v)
}
}
func TestToStrArr(t *testing.T) {
cases := []struct {
in string
out int
}{
{
in: "4",
out: 1,
},
{
in: "a,b,c",
out: 3,
},
{
in: " a, b, c ",
out: 3,
},
{
in: "a,b, c",
out: 3,
},
{
in: "a,b",
out: 2,
},
{
in: "a,",
out: 1,
},
{
in: "a",
out: 1,
},
{
in: "",
out: 0,
},
}
for _, c := range cases {
if len(ToStrArr(c.in)) != c.out {
t.Error(c)
}
}
}
func TestToTime(t *testing.T) {
cases := []struct {
in string
out string
}{
{
in: "2001-02-03 04:05:06",
out: "2001-02-03 04:05:06",
},
{
in: "5",
out: "0001-01-01 00:00:00",
},
}
for _, c := range cases {
time := ToTime(c.in)
if v := time.Format("2006-01-02 15:04:05"); v != c.out {
t.Error(c, v)
}
}
}
func TestToInt(t *testing.T) {
cases := []struct {
in string
out int
}{
{
in: "4",
out: 4,
},
{
in: "a",
out: 0,
},
{
in: "",
out: 0,
},
{
in: "-1",
out: -1,
},
{
in: "5",
out: 5,
},
}
for _, c := range cases {
if ToInt(c.in) != c.out {
t.Error(c)
}
}
}
func testReq() *http.Request {
return httptest.NewRequest("POST", "/path/to", strings.NewReader(`{
"a": "b",
"c": 4,
"d": "e, f,g"
}`))
}

52
server/ajax/list.go Normal file
View File

@@ -0,0 +1,52 @@
package ajax
import (
"errors"
"net/http"
)
type List struct{}
func (a *Ajax) loadLists(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}
func (a *Ajax) changeOrder(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}
func (a *Ajax) addList(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}
func (a *Ajax) renameList(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}
func (a *Ajax) deleteList(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}
func (a *Ajax) setSort(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}
func (a *Ajax) publishList(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}
func (a *Ajax) changeListOrder(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}
func (a *Ajax) clearCompletedInList(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}
func (a *Ajax) setShowNotesInList(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}
func (a *Ajax) setHideList(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}

56
server/ajax/list/list.go Normal file
View File

@@ -0,0 +1,56 @@
package ajax
import (
"errors"
"net/http"
)
type List struct{}
func New(r *http.Request) (*List, error) {
return &List{}, errors.New("not impl")
}
func (l *List) loadLists() error {
return errors.New("not impl")
}
func (l *List) changeOrder() error {
return errors.New("not impl")
}
func (l *List) addList() error {
return errors.New("not impl")
}
func (l *List) renameList() error {
return errors.New("not impl")
}
func (l *List) deleteList() error {
return errors.New("not impl")
}
func (l *List) setSort() error {
return errors.New("not impl")
}
func (l *List) publishList() error {
return errors.New("not impl")
}
func (l *List) changeListOrder() error {
return errors.New("not impl")
}
func (l *List) clearCompletedInList() error {
return errors.New("not impl")
}
func (l *List) setShowNotesInList() error {
return errors.New("not impl")
}
func (l *List) setHideList() error {
return errors.New("not impl")
}

115
server/ajax/storage.go Normal file
View File

@@ -0,0 +1,115 @@
package ajax
import (
"bytes"
"encoding/gob"
"local/todo-server/server/ajax/form"
"local/todo-server/server/ajax/task"
"net/http"
"path"
"strings"
)
func (a *Ajax) Cur(r *http.Request) (string, string, []string) {
listID, _ := a.storageGetCur()
taskID := form.Get(r, "id")
tags, _ := a.storageGetCurTags()
return listID, taskID, tags
}
func (a *Ajax) storageListTasks(listID string, filters ...func(t *task.Task) bool) ([]*task.Task, error) {
results, err := a.DB.List(nil, listID+"/", listID+"/}")
if err != nil {
return nil, err
}
tasks := []*task.Task{}
for _, result := range results {
taskID := strings.TrimPrefix(result, listID+"/")
task, err := a.storageGetTask(listID, taskID)
if err != nil {
return nil, err
}
filtered := true
for _, f := range filters {
if !f(task) {
filtered = false
break
}
}
if filtered {
tasks = append(tasks, task)
}
}
return tasks, nil
}
func (a *Ajax) storageGetTask(listID, taskID string) (*task.Task, error) {
var task task.Task
err := a.storageGet(path.Join(listID, taskID), &task)
return &task, err
}
func (a *Ajax) storageSetTask(listID, taskID string, task *task.Task) error {
return a.storageSet(path.Join(listID, taskID), *task)
}
func (a *Ajax) storageDelTask(listID, taskID string) error {
return a.storageDel(path.Join(listID, taskID))
}
func (a *Ajax) storageGetList(listID string) (*List, error) {
var list List
err := a.storageGet(listID, &list)
return &list, err
}
func (a *Ajax) storageSetList(listID string, list *List) error {
return a.storageSet(listID, *list)
}
func (a *Ajax) storageDelList(listID string) error {
return a.storageDel(listID)
}
func (a *Ajax) storageSetCurTags(tags []string) error {
return a.storageSet("currentTags", tags)
}
func (a *Ajax) storageGetCurTags() ([]string, error) {
var tags []string
err := a.storageGet("currentTags", &tags)
return tags, err
}
func (a *Ajax) storageSetCur(listID string) error {
return a.storageSet("currentList", listID)
}
func (a *Ajax) storageGetCur() (string, error) {
var listID string
err := a.storageGet("currentList", &listID)
return listID, err
}
func (a *Ajax) storageSet(key string, value interface{}) error {
buff := bytes.NewBuffer(nil)
encoder := gob.NewEncoder(buff)
if err := encoder.Encode(value); err != nil {
return err
}
return a.DB.Set(key, buff.Bytes())
}
func (a *Ajax) storageGet(key string, value interface{}) error {
b, err := a.DB.Get(key)
if err != nil {
return err
}
buff := bytes.NewBuffer(b)
decoder := gob.NewDecoder(buff)
return decoder.Decode(value)
}
func (a *Ajax) storageDel(key string) error {
return a.DB.Set(key, nil)
}

14
server/ajax/tag.go Normal file
View File

@@ -0,0 +1,14 @@
package ajax
import (
"errors"
"net/http"
)
func (a *Ajax) suggestTags(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}
func (a *Ajax) tagCloud(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}

128
server/ajax/task.go Normal file
View File

@@ -0,0 +1,128 @@
package ajax
import (
"encoding/json"
"errors"
"fmt"
"local/todo-server/server/ajax/form"
"local/todo-server/server/ajax/task"
"net/http"
"strings"
)
func (a *Ajax) loadTasks(w http.ResponseWriter, r *http.Request) error {
listID, _, _ := a.Cur(r)
filterComplete := func(t *task.Task) bool {
if form.Get(r, "compl") == "" {
return true
}
return t.Complete == (form.Get(r, "compl") != "0")
}
filterTags := func(t *task.Task) bool {
if form.Get(r, "t") == "" {
return true
}
whitelistTags := form.ToStrArr(form.Get(r, "t"))
if len(whitelistTags) == 0 {
return true
}
whitelistTagMap := make(map[string]struct{})
for _, tag := range whitelistTags {
whitelistTagMap[tag] = struct{}{}
}
for _, tag := range t.Tags {
if _, ok := whitelistTagMap[tag]; ok {
return true
}
}
return false
}
filterSubstr := func(t *task.Task) bool {
substr := form.Get(r, "s")
return substr == "" || strings.Contains(fmt.Sprintf("%+v", t), substr)
}
tasks, err := a.storageListTasks(listID, filterComplete, filterTags, filterSubstr)
if err != nil {
return err
}
return json.NewEncoder(w).Encode(map[string]interface{}{"list": tasks})
}
func (a *Ajax) newTask(w http.ResponseWriter, r *http.Request) error {
listID, task, err := a.makeTask(r)
if err != nil {
return err
}
return a.storageSetTask(listID, task.UUID, task)
}
func (a *Ajax) makeTask(r *http.Request) (string, *task.Task, error) {
listID, _, tags := a.Cur(r)
task, err := task.New(r)
if err != nil {
return "", nil, err
}
task.AppendTags(tags)
return listID, task, nil
}
func (a *Ajax) deleteTask(w http.ResponseWriter, r *http.Request) error {
listID, taskID, _ := a.Cur(r)
return a.storageDelTask(listID, taskID)
}
func (a *Ajax) completeTask(w http.ResponseWriter, r *http.Request) error {
listID, taskID, _ := a.Cur(r)
task, err := a.storageGetTask(listID, taskID)
if err != nil {
return err
}
task.SetComplete(form.Get(r, "compl") == "1")
return a.storageSetTask(listID, taskID, task)
}
func (a *Ajax) editNote(w http.ResponseWriter, r *http.Request) error {
listID, taskID, _ := a.Cur(r)
task, err := a.storageGetTask(listID, taskID)
if err != nil {
return err
}
task.SetNote(form.Get(r, "note"))
return a.storageSetTask(listID, taskID, task)
}
func (a *Ajax) editTask(w http.ResponseWriter, r *http.Request) error {
listID, task, err := a.makeTask(r)
if err != nil {
return err
}
_, taskID, _ := a.Cur(r)
task.UUID = taskID
task.ID = task.UUID
return a.storageSetTask(listID, task.UUID, task)
}
func (a *Ajax) setPrio(w http.ResponseWriter, r *http.Request) error {
listID, taskID, _ := a.Cur(r)
task, err := a.storageGetTask(listID, taskID)
if err != nil {
return err
}
task.SetPrio(form.ToInt(form.Get(r, "prio")))
return a.storageSetTask(listID, taskID, task)
}
func (a *Ajax) moveTask(w http.ResponseWriter, r *http.Request) error {
listID, taskID, _ := a.Cur(r)
toList := form.Get(r, "to")
task, err := a.storageGetTask(listID, taskID)
if err != nil {
return err
}
a.storageSetTask(listID, taskID, nil)
return a.storageSetTask(toList, taskID, task)
}
func (a *Ajax) parseTaskStr(w http.ResponseWriter, r *http.Request) error {
return errors.New("not impl")
}

100
server/ajax/task/task.go Normal file
View File

@@ -0,0 +1,100 @@
package task
import (
"errors"
"local/todo-server/server/ajax/form"
"net/http"
"regexp"
"strings"
"time"
"github.com/google/uuid"
)
type Task struct {
ID string
UUID string
Title string
Priority int
Tags []string
Created time.Time
Edited time.Time
Completed time.Time
Complete bool
Note []string
Due time.Time
}
func New(r *http.Request) (*Task, error) {
task := &Task{
UUID: uuid.New().String(),
Title: form.Get(r, "title"),
Priority: form.ToInt(form.Get(r, "prio")),
Tags: form.ToStrArr(form.Get(r, "tags")),
Created: time.Now(),
Edited: time.Now(),
Due: form.ToTime(form.Get(r, "duedate")),
}
task.ID = task.UUID
task.SetNote(form.Get(r, "note"))
return task, task.validate()
}
func (t *Task) AppendTags(tags []string) {
t.touch()
t.Tags = append(t.Tags, tags...)
}
func (t *Task) SetComplete(state bool) {
t.touch()
t.Complete = state
if t.Complete {
t.Completed = time.Now()
} else {
t.Completed = time.Time{}
}
}
func (t *Task) SetPrio(prio int) {
t.touch()
t.Priority = prio
}
func (t *Task) SetNote(note string) {
t.touch()
t.Note = strings.Split(note, "\n")
}
func (t *Task) touch() {
t.Edited = time.Now()
}
func (t *Task) validate() error {
if t.Title == "" {
return errors.New("task cannot have nil title")
}
if err := t.smartSyntax(); err != nil {
return err
}
return nil
}
func (t *Task) smartSyntax() error {
re := regexp.MustCompile(`^(/([+-]{0,1}\d+)?/)?(.*?)(\s+/([^/]*)/$)?$|`)
matches := re.FindAllStringSubmatch(t.Title, 1)[0]
if len(matches) != 6 {
return nil
}
if matches[1] != "" {
t.Priority = form.ToInt(matches[1])
}
if matches[3] != "" {
t.Title = matches[3]
}
if matches[5] != "" {
t.Tags = form.ToStrArr(matches[5])
}
return nil
}

View File

@@ -0,0 +1,59 @@
package task
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func TestNew(t *testing.T) {
if _, err := New(toReq(map[string]interface{}{})); err != nil {
t.Error(err)
}
if _, err := New(toReq(map[string]interface{}{"title": ""})); err == nil {
t.Error(err)
}
if task, err := New(toReq(map[string]interface{}{"title": "i want dogs /a,b,c/"})); err != nil {
t.Error(err)
} else if task.Title != "i want dogs" {
t.Error(task.Title)
} else if fmt.Sprint(task.Tags) != "[a b c]" {
t.Error(task.Tags)
} else {
was := task.Edited
task.touch()
if was == task.Edited {
t.Error(was)
}
was = task.Edited
task.SetNote("hell\nno")
if was == task.Edited {
t.Error(was)
} else if len(task.Note) != 2 {
t.Error(task.Note)
}
}
}
func toReq(m map[string]interface{}) *http.Request {
if m == nil {
m = map[string]interface{}{}
}
els := map[string]interface{}{
"title": "title",
"prio": 1,
"tags": "a, b,c",
"duedate": "2010-02-03 05:06:07",
"note": "hello\nworld\ni\nam a note\nand\ni have\nlots\nof\nlines",
}
for k := range els {
if _, ok := m[k]; !ok {
m[k] = els[k]
}
}
b, _ := json.Marshal(m)
return httptest.NewRequest("POST", "/paht", bytes.NewReader(b))
}

47
server/ajax/task_test.go Normal file
View File

@@ -0,0 +1,47 @@
package ajax
import (
"encoding/json"
"local/todo-server/server/ajax/task"
"net/http"
"net/http/httptest"
"testing"
)
func TestAjaxLoadTasks(t *testing.T) {
a := mockAjax()
func() {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil)
a.loadTasks(w, r)
var result struct {
List []string `json:"list"`
}
if v := w.Code; v != http.StatusOK {
t.Error(v)
} else if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
t.Error(err)
} else if len(result.List) != 0 {
t.Error(result)
}
}()
a.storageSetTask("list", "task", &task.Task{Title: "hi"})
func() {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil)
a.loadTasks(w, r)
var result struct {
List []task.Task `json:"list"`
}
if v := w.Code; v != http.StatusOK {
t.Error(v)
} else if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
t.Error(err)
} else if len(result.List) != 1 {
t.Error(result)
}
}()
}