diff --git a/client/todo/add.go b/client/todo/add.go index a080660..f45116c 100644 --- a/client/todo/add.go +++ b/client/todo/add.go @@ -50,12 +50,24 @@ func CreateNewTodo() common.Todo { for i, l := range labelSlice { labelSlice[i] = strings.TrimSpace(l) } + + sharedInput := common.AskUserString("Shared with (comma separated usernames):\n") + sharedSlice := strings.Split(sharedInput, ",") + var sharedWith []string + for _, s := range sharedSlice { + s = strings.TrimSpace(s) + if s != "" { + sharedWith = append(sharedWith, s) + } + } + return common.Todo{ Name: common.AskUserString("Name:\n"), Description: common.AskUserString("Description:\n"), Status: common.AskUserString(fmt.Sprintf("Status (%s, %s, %s, %s, %s, %s):\n", common.NotStarted, common.Done, common.WIP, common.Pending, common.Blocked, common.Failed)), Labels: labelSlice, + SharedWith: sharedWith, Owner: cfg.Server.Credentials.Username, LastModified: time.Now().UTC(), } diff --git a/client/todo/go.mod b/client/todo/go.mod index c6e223b..c7887fe 100644 --- a/client/todo/go.mod +++ b/client/todo/go.mod @@ -3,7 +3,7 @@ module gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo go 1.25.0 require ( - gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260118191144-e0c04fb9d1e9 + gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404115537-7cf6eabbdd1d github.com/BurntSushi/toml v1.6.0 github.com/urfave/cli/v3 v3.8.0 ) diff --git a/client/todo/go.sum b/client/todo/go.sum index 6cf962e..89e6fc7 100644 --- a/client/todo/go.sum +++ b/client/todo/go.sum @@ -1,5 +1,5 @@ -gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260118191144-e0c04fb9d1e9 h1:EGFIRDjHIEt0IEFYeN2NEn/NyVglN6vXB6IRjm1rN0I= -gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260118191144-e0c04fb9d1e9/go.mod h1:bHquapurFm/eUTtrl9mGLEdAYc5cOeueHFvqjommp44= +gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404115537-7cf6eabbdd1d h1:YpsaXckG7ryvEe7cTwRrGNOB43wLnMlK0vXqYMHdrXQ= +gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404115537-7cf6eabbdd1d/go.mod h1:owENFzNmtoCmr7ZUjNbkO0i+ugwqKdXCVikfOOcOsWk= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= diff --git a/client/todo/todo.go b/client/todo/todo.go index ce52609..0d6f9bf 100644 --- a/client/todo/todo.go +++ b/client/todo/todo.go @@ -141,6 +141,21 @@ func handleUpdate(scanner *bufio.Scanner, todos []common.Todo, store *common.Bol itemToUpdate.Status = newStatus } + fmt.Printf("New shared with (comma separated) [%s]: ", strings.Join(itemToUpdate.SharedWith, ", ")) + scanner.Scan() + newSharedWith := strings.TrimSpace(scanner.Text()) + if newSharedWith != "" { + sharedSlice := strings.Split(newSharedWith, ",") + var sharedWith []string + for _, s := range sharedSlice { + s = strings.TrimSpace(s) + if s != "" { + sharedWith = append(sharedWith, s) + } + } + itemToUpdate.SharedWith = sharedWith + } + err = itemToUpdate.Store(store, cfg.Server.Credentials.Username) if err != nil { slog.Error(err.Error()) diff --git a/common/bolt.go b/common/bolt.go index dbb84e8..d0ebfae 100644 --- a/common/bolt.go +++ b/common/bolt.go @@ -7,6 +7,8 @@ import ( "path/filepath" "sync" + "slices" + bolt "go.etcd.io/bbolt" ) @@ -219,35 +221,53 @@ func (s *BoltStore) ExistsByKey(bucket, key string) (bool, error) { } func (s *BoltStore) GetTodoMap(user string) map[string]Todo { - storedTodoJsons := s.GetAllFromBucket(user) - serverTodos := make(map[string]Todo) - for key, val := range storedTodoJsons { - var todo Todo - if json.Unmarshal([]byte(val), &todo) == nil { - serverTodos[key] = todo - } - } + + s.DB.View(func(tx *bolt.Tx) error { + return tx.ForEach(func(name []byte, b *bolt.Bucket) error { + bucketName := string(name) + c := b.Cursor() + for k, v := c.First(); k != nil; k, v = c.Next() { + var todo Todo + if json.Unmarshal(v, &todo) == nil { + if bucketName == user || slices.Contains(todo.SharedWith, user) { + serverTodos[string(k)] = todo + } + } + } + return nil + }) + }) + return serverTodos } func (s *BoltStore) GetTodoList(user string, includeDeleted bool) []Todo { - storedTodoJsons := s.GetAllFromBucket(user) - var storedTodos []Todo - for _, val := range storedTodoJsons { - var todo Todo - if json.Unmarshal([]byte(val), &todo) == nil { - var include = false - if includeDeleted { - include = true - } else { - include = !todo.Deleted + + s.DB.View(func(tx *bolt.Tx) error { + return tx.ForEach(func(name []byte, b *bolt.Bucket) error { + bucketName := string(name) + c := b.Cursor() + for _, v := c.First(); v != nil; _, v = c.Next() { + var todo Todo + if json.Unmarshal(v, &todo) == nil { + if bucketName == user || slices.Contains(todo.SharedWith, user) { + var include = false + if includeDeleted { + include = true + } else { + include = !todo.Deleted + } + if include { + storedTodos = append(storedTodos, todo) + } + } + } } - if include { - storedTodos = append(storedTodos, todo) - } - } - } + return nil + }) + }) + return storedTodos } diff --git a/common/go.mod b/common/go.mod index 47bc77e..c489a26 100644 --- a/common/go.mod +++ b/common/go.mod @@ -3,9 +3,8 @@ module gitea.kleinsense.nl/DariusKlein/kleinTodo/common go 1.25.0 require ( - github.com/BurntSushi/toml v1.5.0 github.com/charmbracelet/lipgloss v1.1.0 - github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 go.etcd.io/bbolt v1.4.3 golang.org/x/crypto v0.49.0 @@ -18,7 +17,6 @@ require ( github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/common/go.sum b/common/go.sum index 9bfafc7..7c03e1b 100644 --- a/common/go.sum +++ b/common/go.sum @@ -1,5 +1,3 @@ -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= @@ -14,14 +12,12 @@ github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSg github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= @@ -42,15 +38,13 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/common/todo.go b/common/todo.go index 439a5e2..4d9f5c1 100644 --- a/common/todo.go +++ b/common/todo.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "hash/fnv" + "slices" "strings" "time" @@ -12,7 +13,10 @@ import ( ) func (todo *Todo) Store(store *BoltStore, user string) error { - if todo.Owner != user { + isOwner := todo.Owner == user + isShared := slices.Contains(todo.SharedWith, user) + + if !isOwner && !isShared { return fmt.Errorf("unauthorized user") } if todo.Id == "" { @@ -23,7 +27,7 @@ func (todo *Todo) Store(store *BoltStore, user string) error { if err != nil { return err } - return store.SaveValueToBucket(user, todo.Id, string(todoJson)) + return store.SaveValueToBucket(todo.Owner, todo.Id, string(todoJson)) } func (todoRequest StoreTodoRequest) Store(store *BoltStore, user string) error { @@ -33,6 +37,7 @@ func (todoRequest StoreTodoRequest) Store(store *BoltStore, user string) error { Description: todoRequest.Description, Status: todoRequest.Status, Owner: user, + SharedWith: todoRequest.SharedWith, LastModified: time.Now().UTC(), } todoJson, err := json.Marshal(todo) @@ -103,6 +108,9 @@ func (todo Todo) PrintIndexed(index int) { uuidStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) lastMod := todo.LastModified.Format("2006-01-02 15:04") metaLine := fmt.Sprintf(" Last Modified: %s • ID: %s", lastMod, todo.Id) + if len(todo.SharedWith) > 0 { + metaLine = fmt.Sprintf("%s • Shared with: %s", metaLine, strings.Join(todo.SharedWith, ", ")) + } fmt.Println(uuidStyle.Render(metaLine)) fmt.Printf(" %s----------------------------%s\n", ColorGray, ColorReset) @@ -114,6 +122,7 @@ func (t Todo) IsEqual(other Todo) bool { t.Description == other.Description && t.Status == other.Status && t.Owner == other.Owner && + slices.Equal(t.SharedWith, other.SharedWith) && t.Deleted == other.Deleted } @@ -122,5 +131,6 @@ func (t Todo) IsEqualIgnoringStatus(other Todo) bool { t.Name == other.Name && t.Description == other.Description && t.Owner == other.Owner && + slices.Equal(t.SharedWith, other.SharedWith) && t.Deleted == other.Deleted } diff --git a/common/types.go b/common/types.go index fed8e8f..beef494 100644 --- a/common/types.go +++ b/common/types.go @@ -13,15 +13,17 @@ type Todo struct { Description string `json:"description"` Status string `json:"status"` Owner string `json:"owner"` + SharedWith []string `json:"shared_with,omitzero"` Labels []string `json:"labels"` LastModified time.Time `json:"last_modified"` Deleted bool `json:"deleted"` } type StoreTodoRequest struct { - Name string `json:"name"` - Description string `json:"description"` - Status string `json:"status"` + Name string `json:"name"` + Description string `json:"description"` + Status string `json:"status"` + SharedWith []string `json:"shared_with,omitzero"` } type TodoList struct { diff --git a/server/go.mod b/server/go.mod index 18dc3aa..7295442 100644 --- a/server/go.mod +++ b/server/go.mod @@ -2,7 +2,7 @@ module gitea.kleinsense.nl/DariusKlein/kleinTodo/server go 1.25.0 -require gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260118191144-e0c04fb9d1e9 +require gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404115537-7cf6eabbdd1d require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect diff --git a/server/go.sum b/server/go.sum index b53bcc5..4608deb 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1,5 +1,5 @@ -gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260118191144-e0c04fb9d1e9 h1:EGFIRDjHIEt0IEFYeN2NEn/NyVglN6vXB6IRjm1rN0I= -gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260118191144-e0c04fb9d1e9/go.mod h1:bHquapurFm/eUTtrl9mGLEdAYc5cOeueHFvqjommp44= +gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404115537-7cf6eabbdd1d h1:YpsaXckG7ryvEe7cTwRrGNOB43wLnMlK0vXqYMHdrXQ= +gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404115537-7cf6eabbdd1d/go.mod h1:owENFzNmtoCmr7ZUjNbkO0i+ugwqKdXCVikfOOcOsWk= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= diff --git a/tests/sharing_test.go b/tests/sharing_test.go new file mode 100644 index 0000000..06a4bda --- /dev/null +++ b/tests/sharing_test.go @@ -0,0 +1,134 @@ +package tests + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "gitea.kleinsense.nl/DariusKlein/kleinTodo/common" + "gitea.kleinsense.nl/DariusKlein/kleinTodo/server/handler" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSharedTodoSync(t *testing.T) { + // 1. Setup Environment + tempDir, err := os.MkdirTemp("", "todo-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + os.Setenv("XDG_CONFIG_HOME", tempDir) + os.Setenv("JWT_SECRET", "test-secret-123") + + // 2. Initialize Server + mux := http.NewServeMux() + mux.HandleFunc("POST /register", handler.RegisterHandler) + mux.HandleFunc("POST /login", handler.LoginHandler) + mux.HandleFunc("POST /sync", handler.SyncHandler) + mux.HandleFunc("POST /store", handler.StoreHandler) + ts := httptest.NewServer(mux) + defer ts.Close() + + client := ts.Client() + + // 3. Register two users + users := []struct { + username, password string + token string + }{ + {"owner", "pass1", ""}, + {"shared", "pass2", ""}, + } + + for i, u := range users { + regPayload, _ := json.Marshal(common.Credentials{Username: u.username, Password: u.password}) + resp, err := client.Post(ts.URL+"/register", "application/json", bytes.NewBuffer(regPayload)) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + resp, err = client.Post(ts.URL+"/login", "application/json", bytes.NewBuffer(regPayload)) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + users[i].token = resp.Header.Get(common.AuthHeader) + } + + // 4. Owner creates a shared todo + sharedTodo := common.Todo{ + Id: uuid.New().String(), + Name: "Shared Task", + Description: "This is shared", + Status: common.NotStarted, + Owner: "owner", + SharedWith: []string{"shared"}, + LastModified: time.Now().UTC(), + } + + syncReq := common.TodoList{Todos: []common.Todo{sharedTodo}} + syncPayload, _ := json.Marshal(syncReq) + + req, _ := http.NewRequest("POST", ts.URL+"/sync", bytes.NewBuffer(syncPayload)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set(common.AuthHeader, "Bearer "+users[0].token) + + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // 5. Shared user pulls todos + emptySyncReq := common.TodoList{Todos: []common.Todo{}} + emptySyncPayload, _ := json.Marshal(emptySyncReq) + req, _ = http.NewRequest("POST", ts.URL+"/sync", bytes.NewBuffer(emptySyncPayload)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set(common.AuthHeader, "Bearer "+users[1].token) + + resp, err = client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var syncResp common.SyncResponse + err = json.NewDecoder(resp.Body).Decode(&syncResp) + require.NoError(t, err) + resp.Body.Close() + + // Shared user should see the todo + assert.Len(t, syncResp.SyncedTodos, 1) + assert.Equal(t, "Shared Task", syncResp.SyncedTodos[0].Name) + assert.Equal(t, "owner", syncResp.SyncedTodos[0].Owner) + assert.Contains(t, syncResp.SyncedTodos[0].SharedWith, "shared") + + // 6. Shared user updates the todo status + updatedTodo := syncResp.SyncedTodos[0] + updatedTodo.Status = common.Done + updatedTodo.LastModified = time.Now().UTC() + + updateSyncReq := common.TodoList{Todos: []common.Todo{updatedTodo}} + updateSyncPayload, _ := json.Marshal(updateSyncReq) + req, _ = http.NewRequest("POST", ts.URL+"/sync", bytes.NewBuffer(updateSyncPayload)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set(common.AuthHeader, "Bearer "+users[1].token) + + resp, err = client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // 7. Owner pulls todos and checks status + req, _ = http.NewRequest("POST", ts.URL+"/sync", bytes.NewBuffer(emptySyncPayload)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set(common.AuthHeader, "Bearer "+users[0].token) + + resp, err = client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + err = json.NewDecoder(resp.Body).Decode(&syncResp) + require.NoError(t, err) + resp.Body.Close() + + assert.Len(t, syncResp.SyncedTodos, 1) + assert.Equal(t, common.Done, syncResp.SyncedTodos[0].Status, "Owner should see status update from shared user") +}