From e938be1936bd6aed2fa18922694add38f320ac1a Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 18 Apr 2020 18:35:46 +0200 Subject: [PATCH 01/14] Add Kanban bucket handling --- pkg/models/error.go | 33 ++++++++- pkg/models/kanban.go | 132 ++++++++++++++++++++++++++++++++++++ pkg/models/kanban_rights.go | 40 +++++++++++ pkg/models/tasks.go | 3 + 4 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 pkg/models/kanban.go create mode 100644 pkg/models/kanban_rights.go diff --git a/pkg/models/error.go b/pkg/models/error.go index 9ada2ba10..c2a8be9bb 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -1178,7 +1178,7 @@ func IsErrInvalidRight(err error) bool { } func (err ErrInvalidRight) Error() string { - return fmt.Sprintf(" right invalid [Right: %d]", err.Right) + return fmt.Sprintf("Right invalid [Right: %d]", err.Right) } // ErrCodeInvalidRight holds the unique world-error code of this error @@ -1192,3 +1192,34 @@ func (err ErrInvalidRight) HTTPError() web.HTTPError { Message: "The right is invalid.", } } + +// ======== +// Kanban +// ======== + +// ErrBucketDoesNotExist represents an error where a kanban bucket does not exist +type ErrBucketDoesNotExist struct { + BucketID int64 +} + +// IsErrBucketDoesNotExist checks if an error is ErrBucketDoesNotExist. +func IsErrBucketDoesNotExist(err error) bool { + _, ok := err.(ErrBucketDoesNotExist) + return ok +} + +func (err ErrBucketDoesNotExist) Error() string { + return fmt.Sprintf("Bucket does not exist [BucketID: %d]", err.BucketID) +} + +// ErrCodeBucketDoesNotExist holds the unique world-error code of this error +const ErrCodeBucketDoesNotExist = 10001 + +// HTTPError holds the http error description +func (err ErrBucketDoesNotExist) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusNotFound, + Code: ErrCodeBucketDoesNotExist, + Message: "This bucket does not exist.", + } +} diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go new file mode 100644 index 000000000..2d788c28b --- /dev/null +++ b/pkg/models/kanban.go @@ -0,0 +1,132 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2020 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "code.vikunja.io/api/pkg/timeutil" + "code.vikunja.io/api/pkg/user" + "code.vikunja.io/web" + "time" +) + +// Bucket represents a kanban bucket +type Bucket struct { + // The unique, numeric id of this bucket. + ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"bucket"` + Title string `xorm:"text not null" valid:"runelength(1|)" minLength:"1" json:"title"` + ListID int64 `xorm:"int(11) not null" json:"list_id"` + Tasks []*Task `xorm:"-" json:"tasks"` + + // A timestamp when this task was created. You cannot change this value. + Created timeutil.TimeStamp `xorm:"created not null" json:"created"` + // A timestamp when this task was last updated. You cannot change this value. + Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"` + + // The user who initially created the task. + CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"` + CreatedByID int64 `xorm:"int(11) not null" json:"-"` + + web.Rights `xorm:"-" json:"-"` + web.CRUDable `xorm:"-" json:"-"` +} + +func getBucketByID(id int64) (b *Bucket, err error) { + b = &Bucket{} + exists, err := x.Where("id = ?", id).Get(b) + if err != nil { + return + } + if !exists { + return b, ErrBucketDoesNotExist{BucketID: id} + } + return +} + +// Create creates a new bucket +func (b *Bucket) Create(a web.Auth) (err error) { + b.CreatedByID = a.GetID() + + _, err = x.Insert(b) + return +} + +// ReadAll returns all buckets with their tasks for a certain list +func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { + + // Get all buckets for this list + buckets := []*Bucket{ + { + // This is the default bucket for all tasks which are not associated to a bucket. + ID: 0, + Title: "Not associated to a bucket", + ListID: b.ListID, + Created: timeutil.FromTime(time.Now()), + Updated: timeutil.FromTime(time.Now()), + }, + } + + buckets[0].CreatedBy, err = user.GetFromAuth(auth) + if err != nil { + return + } + + err = x.Where("list_id = ?", b.ListID).Find(&buckets) + if err != nil { + return + } + + // Make a map from the bucket slice with their id as key so that we can use it to put the tasks in their buckets + bucketMap := make(map[int64]*Bucket, len(buckets)) + for _, bb := range buckets { + bucketMap[bb.ID] = bb + } + + // Get all tasks for this list + tasks, _, _, err := getTasksForLists([]*List{{ID: b.ListID}}, &taskOptions{}) + if err != nil { + return + } + + for _, task := range tasks { + bucketMap[task.BucketID].Tasks = append(bucketMap[task.BucketID].Tasks, task) + } + + // Put all tasks in their buckets + + // Put all tasks which are not explicitly associated to a bucket into a pseudo bucket + + return buckets, len(buckets), int64(len(buckets)), nil +} + +// Update Updates an existing bucket +func (b *Bucket) Update() (err error) { + _, err = x.Where("id = ?", b.ID).Update(b) + return +} + +// Delete removes a bucket, but no tasks +func (b *Bucket) Delete() (err error) { + // Remove all associations of tasks to that bucket + _, err = x.Where("bucket_id = ?", b.ID).Cols("bucket_id").Update(&Task{BucketID: 0}) + if err != nil { + return + } + + // Remove the bucket itself + _, err = x.Where("id = ?", b.ID).Delete(&Bucket{}) + return +} diff --git a/pkg/models/kanban_rights.go b/pkg/models/kanban_rights.go new file mode 100644 index 000000000..7482661ee --- /dev/null +++ b/pkg/models/kanban_rights.go @@ -0,0 +1,40 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2020 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import "code.vikunja.io/web" + +func (b *Bucket) CanCreate(a web.Auth) (bool, error) { + return b.canDoBucket(a) +} + +func (b *Bucket) CanUpdate(a web.Auth) (bool, error) { + return b.canDoBucket(a) +} + +func (b *Bucket) CanDelete(a web.Auth) (bool, error) { + return b.canDoBucket(a) +} + +func (b *Bucket) canDoBucket(a web.Auth) (bool, error) { + bb, err := getBucketByID(b.ID) + if err != nil { + return false, err + } + l := &List{ID: bb.ListID} + return l.CanWrite(a) +} diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index af76bd1f8..f1d9398fd 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -85,6 +85,9 @@ type Task struct { // A timestamp when this task was last updated. You cannot change this value. Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"` + // BucketID is the ID of the kanban bucket this task belongs to. + BucketID int64 `xorm:"int(11) null" json:"-"` + // The user who initially created the task. CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"` -- 2.40.1 From eaa252b6eea3d0ceb2dfad28c0c59ae1f96adddc Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 18 Apr 2020 22:59:54 +0200 Subject: [PATCH 02/14] Cleanup/Comments/Reorganization --- pkg/models/kanban.go | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 2d788c28b..50d5c76de 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -56,14 +56,6 @@ func getBucketByID(id int64) (b *Bucket, err error) { return } -// Create creates a new bucket -func (b *Bucket) Create(a web.Auth) (err error) { - b.CreatedByID = a.GetID() - - _, err = x.Insert(b) - return -} - // ReadAll returns all buckets with their tasks for a certain list func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { @@ -101,17 +93,24 @@ func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (r return } + // Put all tasks in their buckets + // All tasks which are not associated to any bucket will have bucket id 0 which is the nil value for int64 + // Since we created a bucked with that id at the beginning, all tasks should be in there. for _, task := range tasks { bucketMap[task.BucketID].Tasks = append(bucketMap[task.BucketID].Tasks, task) } - // Put all tasks in their buckets - - // Put all tasks which are not explicitly associated to a bucket into a pseudo bucket - return buckets, len(buckets), int64(len(buckets)), nil } +// Create creates a new bucket +func (b *Bucket) Create(a web.Auth) (err error) { + b.CreatedByID = a.GetID() + + _, err = x.Insert(b) + return +} + // Update Updates an existing bucket func (b *Bucket) Update() (err error) { _, err = x.Where("id = ?", b.ID).Update(b) -- 2.40.1 From 06c2f0707d3733d7c310c96e94d3eb2bf8746580 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 18 Apr 2020 23:09:12 +0200 Subject: [PATCH 03/14] Add migration for buckets --- pkg/migration/20200418230432.go | 43 +++++++++++++++++++++++++++++ pkg/migration/20200418230605.go | 49 +++++++++++++++++++++++++++++++++ pkg/models/kanban.go | 7 +++++ pkg/models/models.go | 1 + 4 files changed, 100 insertions(+) create mode 100644 pkg/migration/20200418230432.go create mode 100644 pkg/migration/20200418230605.go diff --git a/pkg/migration/20200418230432.go b/pkg/migration/20200418230432.go new file mode 100644 index 000000000..213379a43 --- /dev/null +++ b/pkg/migration/20200418230432.go @@ -0,0 +1,43 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2020 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package migration + +import ( + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type task20200418230432 struct { + BucketID int64 `xorm:"int(11) null"` +} + +func (s task20200418230432) TableName() string { + return "tasks" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20200418230432", + Description: "Add bucket id property to task", + Migrate: func(tx *xorm.Engine) error { + return tx.Sync2(task20200418230432{}) + }, + Rollback: func(tx *xorm.Engine) error { + return tx.DropTables(task20200418230432{}) + }, + }) +} diff --git a/pkg/migration/20200418230605.go b/pkg/migration/20200418230605.go new file mode 100644 index 000000000..5cdf1a7d9 --- /dev/null +++ b/pkg/migration/20200418230605.go @@ -0,0 +1,49 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2020 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package migration + +import ( + "code.vikunja.io/api/pkg/timeutil" + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type bucket20200418230605 struct { + ID int64 `xorm:"int(11) autoincr not null unique pk"` + Title string `xorm:"text not null"` + ListID int64 `xorm:"int(11) not null"` + Created timeutil.TimeStamp `xorm:"created not null"` + Updated timeutil.TimeStamp `xorm:"updated not null"` + CreatedByID int64 `xorm:"int(11) not null"` +} + +func (b *bucket20200418230605) TableName() string { + return "buckets" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20200418230605", + Description: "Add bucket table", + Migrate: func(tx *xorm.Engine) error { + return tx.Sync2(bucket20200418230605{}) + }, + Rollback: func(tx *xorm.Engine) error { + return tx.DropTables(bucket20200418230605{}) + }, + }) +} diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 50d5c76de..746dd99af 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -44,6 +44,10 @@ type Bucket struct { web.CRUDable `xorm:"-" json:"-"` } +func (b *Bucket) TableName() string { + return "buckets" +} + func getBucketByID(id int64) (b *Bucket, err error) { b = &Bucket{} exists, err := x.Where("id = ?", id).Get(b) @@ -59,6 +63,9 @@ func getBucketByID(id int64) (b *Bucket, err error) { // ReadAll returns all buckets with their tasks for a certain list func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { + // Note: I'm ignoring pagination for now since I've yet to figure out a way on how to make it work + // I'll probably just don't do it and instead make individual tasks archivable. + // Get all buckets for this list buckets := []*Bucket{ { diff --git a/pkg/models/models.go b/pkg/models/models.go index 5ad208bf0..873aa6c50 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -51,6 +51,7 @@ func GetTables() []interface{} { &TaskRelation{}, &TaskAttachment{}, &TaskComment{}, + &Bucket{}, } } -- 2.40.1 From 2da2837942f6fcd12a1c92933095f645289346f3 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 18 Apr 2020 23:12:50 +0200 Subject: [PATCH 04/14] Fix lint --- pkg/models/kanban.go | 18 +++++++++++------- pkg/models/kanban_rights.go | 7 ++++++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 746dd99af..547b237dc 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -26,17 +26,20 @@ import ( // Bucket represents a kanban bucket type Bucket struct { // The unique, numeric id of this bucket. - ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"bucket"` - Title string `xorm:"text not null" valid:"runelength(1|)" minLength:"1" json:"title"` - ListID int64 `xorm:"int(11) not null" json:"list_id"` - Tasks []*Task `xorm:"-" json:"tasks"` + ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"bucket"` + // The title of this bucket. + Title string `xorm:"text not null" valid:"runelength(1|)" minLength:"1" json:"title"` + // The list this bucket belongs to. + ListID int64 `xorm:"int(11) not null" json:"list_id"` + // All tasks which belong to this bucket. + Tasks []*Task `xorm:"-" json:"tasks"` - // A timestamp when this task was created. You cannot change this value. + // A timestamp when this bucket was created. You cannot change this value. Created timeutil.TimeStamp `xorm:"created not null" json:"created"` - // A timestamp when this task was last updated. You cannot change this value. + // A timestamp when this bucket was last updated. You cannot change this value. Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"` - // The user who initially created the task. + // The user who initially created the bucket. CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"` CreatedByID int64 `xorm:"int(11) not null" json:"-"` @@ -44,6 +47,7 @@ type Bucket struct { web.CRUDable `xorm:"-" json:"-"` } +// TableName returns the table name for this bucket. func (b *Bucket) TableName() string { return "buckets" } diff --git a/pkg/models/kanban_rights.go b/pkg/models/kanban_rights.go index 7482661ee..cdf386812 100644 --- a/pkg/models/kanban_rights.go +++ b/pkg/models/kanban_rights.go @@ -18,18 +18,23 @@ package models import "code.vikunja.io/web" +// CanCreate checks if a user can create a new bucket func (b *Bucket) CanCreate(a web.Auth) (bool, error) { - return b.canDoBucket(a) + l := &List{ID: b.ListID} + return l.CanWrite(a) } +// CanUpdate checks if a user can update an existing bucket func (b *Bucket) CanUpdate(a web.Auth) (bool, error) { return b.canDoBucket(a) } +// CanDelete checks if a user can delete an existing bucket func (b *Bucket) CanDelete(a web.Auth) (bool, error) { return b.canDoBucket(a) } +// canDoBucket checks if the bucket exists and if the user has the right to act on it func (b *Bucket) canDoBucket(a web.Auth) (bool, error) { bb, err := getBucketByID(b.ID) if err != nil { -- 2.40.1 From f027b744efd145724ad5d8033ed1c8c9fa929914 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 18 Apr 2020 23:22:40 +0200 Subject: [PATCH 05/14] Fix log level when testing --- pkg/db/db.go | 2 +- pkg/db/test.go | 2 +- pkg/log/xorm_logger.go | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/db/db.go b/pkg/db/db.go index 4e4615052..713ace74a 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -70,7 +70,7 @@ func CreateDBEngine() (engine *xorm.Engine, err error) { } engine.SetMapper(core.GonicMapper{}) - logger := log.NewXormLogger() + logger := log.NewXormLogger("") engine.SetLogger(logger) // Cache diff --git a/pkg/db/test.go b/pkg/db/test.go index 8752dd47b..f6274e368 100644 --- a/pkg/db/test.go +++ b/pkg/db/test.go @@ -46,7 +46,7 @@ func CreateTestEngine() (engine *xorm.Engine, err error) { } engine.SetMapper(core.GonicMapper{}) - logger := log.NewXormLogger() + logger := log.NewXormLogger("DEBUG") logger.ShowSQL(os.Getenv("UNIT_TESTS_VERBOSE") == "1") engine.SetLogger(logger) x = engine diff --git a/pkg/log/xorm_logger.go b/pkg/log/xorm_logger.go index a3f3dcb48..f948118dd 100644 --- a/pkg/log/xorm_logger.go +++ b/pkg/log/xorm_logger.go @@ -37,8 +37,11 @@ type XormLogger struct { } // NewXormLogger creates and initializes a new xorm logger -func NewXormLogger() *XormLogger { - level, err := logging.LogLevel(strings.ToUpper(config.LogDatabaseLevel.GetString())) +func NewXormLogger(lvl string) *XormLogger { + if lvl == "" { + lvl = strings.ToUpper(config.LogDatabaseLevel.GetString()) + } + level, err := logging.LogLevel(lvl) if err != nil { Critical("Error setting database log level: %s", err.Error()) } -- 2.40.1 From de20a17455d953f929fbc87f9f49dcbc368be6ff Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 18 Apr 2020 23:42:42 +0200 Subject: [PATCH 06/14] Add getting users of a bucket --- pkg/models/kanban.go | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 547b237dc..1602e18a8 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -74,11 +74,12 @@ func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (r buckets := []*Bucket{ { // This is the default bucket for all tasks which are not associated to a bucket. - ID: 0, - Title: "Not associated to a bucket", - ListID: b.ListID, - Created: timeutil.FromTime(time.Now()), - Updated: timeutil.FromTime(time.Now()), + ID: 0, + Title: "Not associated to a bucket", + ListID: b.ListID, + Created: timeutil.FromTime(time.Now()), + Updated: timeutil.FromTime(time.Now()), + CreatedByID: auth.GetID(), }, } @@ -94,8 +95,21 @@ func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (r // Make a map from the bucket slice with their id as key so that we can use it to put the tasks in their buckets bucketMap := make(map[int64]*Bucket, len(buckets)) + userIDs := make([]int64, 0, len(buckets)) for _, bb := range buckets { bucketMap[bb.ID] = bb + userIDs = append(userIDs, bb.CreatedByID) + } + + // Get all users + users := make(map[int64]*user.User) + err = x.In("id", userIDs).Find(&users) + if err != nil { + return + } + + for _, bb := range buckets { + bb.CreatedBy = users[bb.CreatedByID] } // Get all tasks for this list -- 2.40.1 From 8e1a2c046c8476304e75ebf37d10f13fc163e258 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 18 Apr 2020 23:43:09 +0200 Subject: [PATCH 07/14] Add tests --- pkg/db/fixtures/buckets.yml | 24 +++++++++++++ pkg/db/fixtures/tasks.yml | 8 +++++ pkg/models/kanban_test.go | 67 +++++++++++++++++++++++++++++++++++++ pkg/models/unit_tests.go | 4 ++- 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 pkg/db/fixtures/buckets.yml create mode 100644 pkg/models/kanban_test.go diff --git a/pkg/db/fixtures/buckets.yml b/pkg/db/fixtures/buckets.yml new file mode 100644 index 000000000..066d407af --- /dev/null +++ b/pkg/db/fixtures/buckets.yml @@ -0,0 +1,24 @@ +- id: 1 + title: testbucket1 + list_id: 1 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 2 + title: testbucket2 + list_id: 1 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 3 + title: testbucket3 + list_id: 1 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 4 + title: testbucket4 - other list + list_id: 2 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 diff --git a/pkg/db/fixtures/tasks.yml b/pkg/db/fixtures/tasks.yml index 72eb30185..7ee8f7c4c 100644 --- a/pkg/db/fixtures/tasks.yml +++ b/pkg/db/fixtures/tasks.yml @@ -7,6 +7,7 @@ index: 1 created: 1543626724 updated: 1543626724 + bucket_id: 1 - id: 2 text: 'task #2 done' done: true @@ -15,6 +16,7 @@ index: 2 created: 1543626724 updated: 1543626724 + bucket_id: 1 - id: 3 text: 'task #3 high prio' done: false @@ -24,6 +26,7 @@ created: 1543626724 updated: 1543626724 priority: 100 + bucket_id: 2 - id: 4 text: 'task #4 low prio' done: false @@ -33,6 +36,7 @@ created: 1543626724 updated: 1543626724 priority: 1 + bucket_id: 2 - id: 5 text: 'task #5 higher due date' done: false @@ -42,6 +46,7 @@ created: 1543626724 updated: 1543626724 due_date_unix: 1543636724 + bucket_id: 2 - id: 6 text: 'task #6 lower due date' done: false @@ -51,6 +56,7 @@ created: 1543626724 updated: 1543626724 due_date_unix: 1543616724 + bucket_id: 3 - id: 7 text: 'task #7 with start date' done: false @@ -60,6 +66,7 @@ created: 1543626724 updated: 1543626724 start_date_unix: 1544600000 + bucket_id: 3 - id: 8 text: 'task #8 with end date' done: false @@ -69,6 +76,7 @@ created: 1543626724 updated: 1543626724 end_date_unix: 1544700000 + bucket_id: 3 - id: 9 text: 'task #9 with start and end date' done: false diff --git a/pkg/models/kanban_test.go b/pkg/models/kanban_test.go new file mode 100644 index 000000000..52b77241e --- /dev/null +++ b/pkg/models/kanban_test.go @@ -0,0 +1,67 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2020 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package models + +import ( + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/user" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestBucket_ReadAll(t *testing.T) { + db.LoadAndAssertFixtures(t) + + testuser := &user.User{ID: 1} + b := &Bucket{ListID: 1} + bucketsInterface, _, _, err := b.ReadAll(testuser, "", 0, 0) + assert.NoError(t, err) + + buckets, is := bucketsInterface.([]*Bucket) + assert.True(t, is) + + // Assert that we have a user for each bucket + assert.Equal(t, testuser.ID, buckets[0].CreatedBy.ID) + assert.Equal(t, testuser.ID, buckets[1].CreatedBy.ID) + assert.Equal(t, testuser.ID, buckets[2].CreatedBy.ID) + assert.Equal(t, testuser.ID, buckets[3].CreatedBy.ID) + + // Assert our three test buckets + one for all tasks without a bucket + assert.Len(t, buckets, 4) + + // Assert all tasks are in the right bucket + assert.Len(t, buckets[0].Tasks, 10) + assert.Len(t, buckets[1].Tasks, 2) + assert.Len(t, buckets[2].Tasks, 3) + assert.Len(t, buckets[3].Tasks, 3) + + // Assert we have bucket 0, 1, 2, 3 but not 4 (that belongs to a different list) + assert.Equal(t, int64(1), buckets[1].ID) + assert.Equal(t, int64(2), buckets[2].ID) + assert.Equal(t, int64(3), buckets[3].ID) + + // Kinda assert all tasks are in the right buckets + assert.Equal(t, int64(0), buckets[0].Tasks[0].BucketID) + assert.Equal(t, int64(1), buckets[1].Tasks[0].BucketID) + assert.Equal(t, int64(1), buckets[1].Tasks[1].BucketID) + assert.Equal(t, int64(2), buckets[2].Tasks[0].BucketID) + assert.Equal(t, int64(2), buckets[2].Tasks[1].BucketID) + assert.Equal(t, int64(2), buckets[2].Tasks[2].BucketID) + assert.Equal(t, int64(3), buckets[3].Tasks[0].BucketID) + assert.Equal(t, int64(3), buckets[3].Tasks[1].BucketID) + assert.Equal(t, int64(3), buckets[3].Tasks[2].BucketID) +} diff --git a/pkg/models/unit_tests.go b/pkg/models/unit_tests.go index 442a6be0f..4fc373205 100644 --- a/pkg/models/unit_tests.go +++ b/pkg/models/unit_tests.go @@ -56,7 +56,9 @@ func SetupTests() { "teams", "users", "users_list", - "users_namespace") + "users_namespace", + "buckets", + ) if err != nil { log.Fatal(err) } -- 2.40.1 From 43eb8f4af51b82a7876134d9859b96d9be31e2f1 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 18 Apr 2020 23:52:38 +0200 Subject: [PATCH 08/14] Make sure a bucket and a task belong to the same list when adding or updating a task --- pkg/models/error.go | 28 ++++++++++++++++++++++++++++ pkg/models/tasks.go | 30 +++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/pkg/models/error.go b/pkg/models/error.go index c2a8be9bb..3b266af96 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -1223,3 +1223,31 @@ func (err ErrBucketDoesNotExist) HTTPError() web.HTTPError { Message: "This bucket does not exist.", } } + +// ErrBucketDoesNotBelongToList represents an error where a kanban bucket does not belong to a list +type ErrBucketDoesNotBelongToList struct { + BucketID int64 + ListID int64 +} + +// IsErrBucketDoesNotBelongToList checks if an error is ErrBucketDoesNotBelongToList. +func IsErrBucketDoesNotBelongToList(err error) bool { + _, ok := err.(ErrBucketDoesNotBelongToList) + return ok +} + +func (err ErrBucketDoesNotBelongToList) Error() string { + return fmt.Sprintf("Bucket does not not belong to list [BucketID: %d, ListID: %d]", err.BucketID, err.ListID) +} + +// ErrCodeBucketDoesNotBelongToList holds the unique world-error code of this error +const ErrCodeBucketDoesNotBelongToList = 10002 + +// HTTPError holds the http error description +func (err ErrBucketDoesNotBelongToList) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusBadRequest, + Code: ErrCodeBucketDoesNotBelongToList, + Message: "This bucket does not belong to that list.", + } +} diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index f1d9398fd..d5e2961ec 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -86,7 +86,7 @@ type Task struct { Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"` // BucketID is the ID of the kanban bucket this task belongs to. - BucketID int64 `xorm:"int(11) null" json:"-"` + BucketID int64 `xorm:"int(11) null" json:"bucket_id"` // The user who initially created the task. CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"` @@ -462,6 +462,22 @@ func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) { return } +func checkBucketAndTaskBelongToSameList(t *Task) (err error) { + if t.BucketID != 0 { + b, err := getBucketByID(t.BucketID) + if err != nil { + return + } + if t.ListID != b.ListID { + return ErrBucketDoesNotBelongToList{ + ListID: t.ListID, + BucketID: t.BucketID, + } + } + } + return +} + // Create is the implementation to create a list task // @Summary Create a task // @Description Inserts a task into a list. @@ -501,6 +517,12 @@ func (t *Task) Create(a web.Auth) (err error) { t.UID = utils.MakeRandomString(40) } + // If there is a bucket set, make sure they belong to the same list as the task + err = checkBucketAndTaskBelongToSameList(t) + if err != nil { + return + } + // Get the index for this task latestTask := &Task{} _, err = x.Where("list_id = ?", t.ListID).OrderBy("id desc").Get(latestTask) @@ -576,6 +598,12 @@ func (t *Task) Update() (err error) { return err } + // If there is a bucket set, make sure they belong to the same list as the task + err = checkBucketAndTaskBelongToSameList(t) + if err != nil { + return + } + // Update the labels // // Maybe FIXME: -- 2.40.1 From e434ca9a13b08dc0c1e84a8bbfa88934327eb9fd Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 18 Apr 2020 23:53:46 +0200 Subject: [PATCH 09/14] Fix err shadow --- pkg/models/tasks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index d5e2961ec..f4c4619ad 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -466,7 +466,7 @@ func checkBucketAndTaskBelongToSameList(t *Task) (err error) { if t.BucketID != 0 { b, err := getBucketByID(t.BucketID) if err != nil { - return + return err } if t.ListID != b.ListID { return ErrBucketDoesNotBelongToList{ -- 2.40.1 From 10ac1c5a17a3f664ba536d849e225b90448fec1f Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 19 Apr 2020 00:06:14 +0200 Subject: [PATCH 10/14] Fix tests --- pkg/integrations/task_collection_test.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/integrations/task_collection_test.go b/pkg/integrations/task_collection_test.go index 05bb7c07e..fcd04ce2e 100644 --- a/pkg/integrations/task_collection_test.go +++ b/pkg/integrations/task_collection_test.go @@ -95,33 +95,33 @@ func TestTaskCollection(t *testing.T) { t.Run("by priority", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) }) t.Run("by priority desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`) + assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`) }) t.Run("by priority asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) }) // should equal duedate asc t.Run("by due_date", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) }) t.Run("by duedate desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`) + assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`) }) t.Run("by duedate asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"asc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) }) t.Run("invalid sort parameter", func(t *testing.T) { _, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams) @@ -270,33 +270,33 @@ func TestTaskCollection(t *testing.T) { t.Run("by priority", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) }) t.Run("by priority desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`) + assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`) }) t.Run("by priority asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) }) // should equal duedate asc t.Run("by due_date", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) }) t.Run("by duedate desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`) + assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`) }) t.Run("by duedate asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"asc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`) }) t.Run("invalid parameter", func(t *testing.T) { // Invalid parameter should not sort at all -- 2.40.1 From 75a7b3ec8ab4bcf30cf04e086caaf8fbc40f83bc Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 19 Apr 2020 00:38:37 +0200 Subject: [PATCH 11/14] Add integration tests --- pkg/db/fixtures/buckets.yml | 79 +++++++++ pkg/integrations/kanban_test.go | 300 ++++++++++++++++++++++++++++++++ pkg/integrations/task_test.go | 36 ++++ pkg/models/kanban.go | 4 +- pkg/models/tasks.go | 16 +- 5 files changed, 425 insertions(+), 10 deletions(-) create mode 100644 pkg/integrations/kanban_test.go diff --git a/pkg/db/fixtures/buckets.yml b/pkg/db/fixtures/buckets.yml index 066d407af..d7d23b729 100644 --- a/pkg/db/fixtures/buckets.yml +++ b/pkg/db/fixtures/buckets.yml @@ -22,3 +22,82 @@ created_by_id: 1 created: 1587244432 updated: 1587244432 +# The following are not or only partly owned by user 1 +- id: 5 + title: testbucket5 + list_id: 20 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 6 + title: testbucket6 + list_id: 6 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 7 + title: testbucket7 + list_id: 7 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 8 + title: testbucket8 + list_id: 8 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 9 + title: testbucket9 + list_id: 9 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 10 + title: testbucket10 + list_id: 10 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 11 + title: testbucket11 + list_id: 11 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 12 + title: testbucket13 + list_id: 12 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 13 + title: testbucket13 + list_id: 13 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 14 + title: testbucket14 + list_id: 14 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 15 + title: testbucket15 + list_id: 15 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 16 + title: testbucket16 + list_id: 16 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 +- id: 17 + title: testbucket17 + list_id: 17 + created_by_id: 1 + created: 1587244432 + updated: 1587244432 diff --git a/pkg/integrations/kanban_test.go b/pkg/integrations/kanban_test.go new file mode 100644 index 000000000..5d30e13e9 --- /dev/null +++ b/pkg/integrations/kanban_test.go @@ -0,0 +1,300 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2020 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package integrations + +import ( + "code.vikunja.io/api/pkg/models" + "code.vikunja.io/web/handler" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestBucket(t *testing.T) { + testHandler := webHandlerTest{ + user: &testuser1, + strFunc: func() handler.CObject { + return &models.Bucket{} + }, + t: t, + } + t.Run("ReadAll", func(t *testing.T) { + t.Run("Normal", func(t *testing.T) { + rec, err := testHandler.testReadAllWithUser(nil, map[string]string{"list": "1"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `testbucket1`) + assert.Contains(t, rec.Body.String(), `testbucket2`) + assert.Contains(t, rec.Body.String(), `testbucket3`) + assert.NotContains(t, rec.Body.String(), `testbucket4`) // Different List + }) + }) + t.Run("Update", func(t *testing.T) { + t.Run("Normal", func(t *testing.T) { + // Check the list was loaded successfully afterwards, see testReadOneWithUser + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "1"}, `{"title":"TestLoremIpsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) + }) + t.Run("Nonexisting Bucket", func(t *testing.T) { + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "9999"}, `{"title":"TestLoremIpsum"}`) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist) + }) + t.Run("Empty title", func(t *testing.T) { + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "1"}, `{"title":""}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required") + }) + t.Run("Rights check", func(t *testing.T) { + t.Run("Forbidden", func(t *testing.T) { + // Owned by user13 + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "5"}, `{"title":"TestLoremIpsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via Team readonly", func(t *testing.T) { + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "6"}, `{"title":"TestLoremIpsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via Team write", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "7"}, `{"title":"TestLoremIpsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) + }) + t.Run("Shared Via Team admin", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "8"}, `{"title":"TestLoremIpsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) + }) + + t.Run("Shared Via User readonly", func(t *testing.T) { + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "9"}, `{"title":"TestLoremIpsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via User write", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "10"}, `{"title":"TestLoremIpsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) + }) + t.Run("Shared Via User admin", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "11"}, `{"title":"TestLoremIpsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) + }) + + t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "12"}, `{"title":"TestLoremIpsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "13"}, `{"title":"TestLoremIpsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) + }) + t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "14"}, `{"title":"TestLoremIpsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) + }) + + t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "15"}, `{"title":"TestLoremIpsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via NamespaceUser write", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "16"}, `{"title":"TestLoremIpsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) + }) + t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "17"}, `{"title":"TestLoremIpsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) + }) + }) + }) + t.Run("Delete", func(t *testing.T) { + t.Run("Normal", func(t *testing.T) { + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "1"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) + }) + t.Run("Nonexisting", func(t *testing.T) { + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "999"}) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist) + }) + t.Run("Rights check", func(t *testing.T) { + t.Run("Forbidden", func(t *testing.T) { + // Owned by user13 + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "5"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via Team readonly", func(t *testing.T) { + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "6"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via Team write", func(t *testing.T) { + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "7"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) + }) + t.Run("Shared Via Team admin", func(t *testing.T) { + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "8"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) + }) + + t.Run("Shared Via User readonly", func(t *testing.T) { + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "9"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via User write", func(t *testing.T) { + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "10"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) + }) + t.Run("Shared Via User admin", func(t *testing.T) { + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "11"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) + }) + + t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "12"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "13"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) + }) + t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "14"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) + }) + + t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { + _, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "15"}) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via NamespaceUser write", func(t *testing.T) { + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "16"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) + }) + t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { + rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "17"}) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) + }) + }) + }) + t.Run("Create", func(t *testing.T) { + t.Run("Normal", func(t *testing.T) { + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`) + }) + t.Run("Nonexisting", func(t *testing.T) { + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9999"}, `{"title":"Lorem Ipsum"}`) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist) + }) + t.Run("Rights check", func(t *testing.T) { + t.Run("Forbidden", func(t *testing.T) { + // Owned by user13 + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"title":"Lorem Ipsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via Team readonly", func(t *testing.T) { + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "6"}, `{"title":"Lorem Ipsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via Team write", func(t *testing.T) { + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "7"}, `{"title":"Lorem Ipsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`) + }) + t.Run("Shared Via Team admin", func(t *testing.T) { + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "8"}, `{"title":"Lorem Ipsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`) + }) + + t.Run("Shared Via User readonly", func(t *testing.T) { + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"title":"Lorem Ipsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via User write", func(t *testing.T) { + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"title":"Lorem Ipsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`) + }) + t.Run("Shared Via User admin", func(t *testing.T) { + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"title":"Lorem Ipsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`) + }) + + t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "12"}, `{"title":"Lorem Ipsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "13"}, `{"title":"Lorem Ipsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`) + }) + t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "14"}, `{"title":"Lorem Ipsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`) + }) + + t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "15"}, `{"title":"Lorem Ipsum"}`) + assert.Error(t, err) + assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) + }) + t.Run("Shared Via NamespaceUser write", func(t *testing.T) { + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "16"}, `{"title":"Lorem Ipsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`) + }) + t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "17"}, `{"title":"Lorem Ipsum"}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`) + }) + }) + }) +} diff --git a/pkg/integrations/task_test.go b/pkg/integrations/task_test.go index c5cc1078a..a428a7bfd 100644 --- a/pkg/integrations/task_test.go +++ b/pkg/integrations/task_test.go @@ -287,6 +287,24 @@ func TestTask(t *testing.T) { assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden) }) }) + t.Run("Bucket", func(t *testing.T) { + t.Run("Normal", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"bucket_id":2}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"bucket_id":2`) + assert.NotContains(t, rec.Body.String(), `"bucket_id":1`) + }) + t.Run("Different List", func(t *testing.T) { + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"bucket_id":4}`) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToList) + }) + t.Run("Nonexisting Bucket", func(t *testing.T) { + _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"bucket_id":9999}`) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist) + }) + }) }) t.Run("Delete", func(t *testing.T) { t.Run("Normal", func(t *testing.T) { @@ -452,5 +470,23 @@ func TestTask(t *testing.T) { assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) }) }) + t.Run("Bucket", func(t *testing.T) { + t.Run("Normal", func(t *testing.T) { + rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum","bucket_id":2}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"bucket_id":2`) + assert.NotContains(t, rec.Body.String(), `"bucket_id":1`) + }) + t.Run("Different List", func(t *testing.T) { + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum","bucket_id":4}`) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToList) + }) + t.Run("Nonexisting Bucket", func(t *testing.T) { + _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum","bucket_id":9999}`) + assert.Error(t, err) + assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist) + }) + }) }) } diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 1602e18a8..f27738bb8 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -28,9 +28,9 @@ type Bucket struct { // The unique, numeric id of this bucket. ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"bucket"` // The title of this bucket. - Title string `xorm:"text not null" valid:"runelength(1|)" minLength:"1" json:"title"` + Title string `xorm:"text not null" valid:"required" minLength:"1" json:"title"` // The list this bucket belongs to. - ListID int64 `xorm:"int(11) not null" json:"list_id"` + ListID int64 `xorm:"int(11) not null" json:"list_id" param:"list"` // All tasks which belong to this bucket. Tasks []*Task `xorm:"-" json:"tasks"` diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index f4c4619ad..9740fd095 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -462,16 +462,16 @@ func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) { return } -func checkBucketAndTaskBelongToSameList(t *Task) (err error) { - if t.BucketID != 0 { - b, err := getBucketByID(t.BucketID) +func checkBucketAndTaskBelongToSameList(fullTask *Task, bucketID int64) (err error) { + if bucketID != 0 { + b, err := getBucketByID(bucketID) if err != nil { return err } - if t.ListID != b.ListID { + if fullTask.ListID != b.ListID { return ErrBucketDoesNotBelongToList{ - ListID: t.ListID, - BucketID: t.BucketID, + ListID: fullTask.ListID, + BucketID: fullTask.BucketID, } } } @@ -518,7 +518,7 @@ func (t *Task) Create(a web.Auth) (err error) { } // If there is a bucket set, make sure they belong to the same list as the task - err = checkBucketAndTaskBelongToSameList(t) + err = checkBucketAndTaskBelongToSameList(t, t.BucketID) if err != nil { return } @@ -599,7 +599,7 @@ func (t *Task) Update() (err error) { } // If there is a bucket set, make sure they belong to the same list as the task - err = checkBucketAndTaskBelongToSameList(t) + err = checkBucketAndTaskBelongToSameList(&ot, t.BucketID) if err != nil { return } -- 2.40.1 From 3670e2ecdb78d9a969fe64c8435fa4e1beb833f3 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 19 Apr 2020 00:55:48 +0200 Subject: [PATCH 12/14] Add swagger docs for bucket endpoints --- pkg/models/kanban.go | 49 ++++++++++++++++++++++++++++++++++++++++++++ pkg/routes/routes.go | 10 +++++++++ 2 files changed, 59 insertions(+) diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index f27738bb8..40a4db2d4 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -65,6 +65,16 @@ func getBucketByID(id int64) (b *Bucket, err error) { } // ReadAll returns all buckets with their tasks for a certain list +// @Summary Get all kanban buckets of a list +// @Description Returns all kanban buckets with belong to a list including their tasks. +// @tags task +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param id path int true "List Id" +// @Success 200 {array} models.Bucket "The buckets with their tasks" +// @Failure 500 {object} models.Message "Internal server error" +// @Router /lists/{id}/buckets [get] func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { // Note: I'm ignoring pagination for now since I've yet to figure out a way on how to make it work @@ -129,6 +139,19 @@ func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (r } // Create creates a new bucket +// @Summary Create a new bucket +// @Description Creates a new kanban bucket on a list. +// @tags task +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param id path int true "List Id" +// @Param bucket body models.Bucket true "The bucket object" +// @Success 200 {object} models.Bucket "The created bucket object." +// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid bucket object provided." +// @Failure 404 {object} code.vikunja.io/web.HTTPError "The list does not exist." +// @Failure 500 {object} models.Message "Internal error" +// @Router /lists/{id}/buckets [put] func (b *Bucket) Create(a web.Auth) (err error) { b.CreatedByID = a.GetID() @@ -137,12 +160,38 @@ func (b *Bucket) Create(a web.Auth) (err error) { } // Update Updates an existing bucket +// @Summary Update an existing bucket +// @Description Updates an existing kanban bucket. +// @tags task +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param listID path int true "List Id" +// @Param bucketID path int true "Bucket Id" +// @Param bucket body models.Bucket true "The bucket object" +// @Success 200 {object} models.Bucket "The created bucket object." +// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid bucket object provided." +// @Failure 404 {object} code.vikunja.io/web.HTTPError "The bucket does not exist." +// @Failure 500 {object} models.Message "Internal error" +// @Router /lists/{listID}/buckets/{bucketID} [post] func (b *Bucket) Update() (err error) { _, err = x.Where("id = ?", b.ID).Update(b) return } // Delete removes a bucket, but no tasks +// @Summary Deletes an existing bucket +// @Description Deletes an existing kanban bucket and dissociates all of its task. It does not delete any tasks. +// @tags task +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param listID path int true "List Id" +// @Param bucketID path int true "Bucket Id" +// @Success 200 {object} models.Message "Successfully deleted." +// @Failure 404 {object} code.vikunja.io/web.HTTPError "The bucket does not exist." +// @Failure 500 {object} models.Message "Internal error" +// @Router /lists/{listID}/buckets/{bucketID} [delete] func (b *Bucket) Delete() (err error) { // Remove all associations of tasks to that bucket _, err = x.Where("bucket_id = ?", b.ID).Cols("bucket_id").Update(&Task{BucketID: 0}) diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index f6136d9a6..af5c53a04 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -246,6 +246,16 @@ func registerAPIRoutes(a *echo.Group) { } a.GET("/lists/:list/tasks", taskCollectionHandler.ReadAllWeb) + kanbanBucketHandler := &handler.WebHandler{ + EmptyStruct: func() handler.CObject { + return &models.Bucket{} + }, + } + a.GET("/lists/:list/buckets", kanbanBucketHandler.ReadAllWeb) + a.PUT("/lists/:list/buckets", kanbanBucketHandler.CreateWeb) + a.POST("/lists/:list/buckets/:bucket", kanbanBucketHandler.UpdateWeb) + a.DELETE("/lists/:list/buckets/:bucket", kanbanBucketHandler.DeleteWeb) + taskHandler := &handler.WebHandler{ EmptyStruct: func() handler.CObject { return &models.Task{} -- 2.40.1 From bc4fe0f22812fe5a6409e01d257aac8f327cb79b Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 19 Apr 2020 00:57:19 +0200 Subject: [PATCH 13/14] Add error docs --- docs/content/doc/usage/errors.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/content/doc/usage/errors.md b/docs/content/doc/usage/errors.md index 1077a1142..93fd23408 100644 --- a/docs/content/doc/usage/errors.md +++ b/docs/content/doc/usage/errors.md @@ -73,4 +73,6 @@ This document describes the different errors Vikunja can return. | 8001 | 403 | This label already exists on that task. | | 8002 | 404 | The label does not exist. | | 8003 | 403 | The user does not have access to this label. | -| 9001 | 403 | The right is invalid. | \ No newline at end of file +| 9001 | 403 | The right is invalid. | +| 10001 | 404 | The bucket does not exist. | +| 10002 | 400 | The bucket does not belong to that list. | -- 2.40.1 From 8a6aecaa9523d96d3c6a0c02e255c9afb2b83da0 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 19 Apr 2020 01:27:17 +0200 Subject: [PATCH 14/14] Fix tests --- pkg/models/task_collection_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index fddc7560e..25abfac3d 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -56,6 +56,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { CreatedByID: 1, CreatedBy: user1, ListID: 1, + BucketID: 1, Labels: []*Label{ { ID: 4, @@ -114,6 +115,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { CreatedByID: 1, CreatedBy: user1, ListID: 1, + BucketID: 1, Labels: []*Label{ { ID: 4, @@ -140,6 +142,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { Created: 1543626724, Updated: 1543626724, Priority: 100, + BucketID: 2, } task4 := &Task{ ID: 4, @@ -153,6 +156,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { Created: 1543626724, Updated: 1543626724, Priority: 1, + BucketID: 2, } task5 := &Task{ ID: 5, @@ -166,6 +170,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { Created: 1543626724, Updated: 1543626724, DueDate: 1543636724, + BucketID: 2, } task6 := &Task{ ID: 6, @@ -179,6 +184,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { Created: 1543626724, Updated: 1543626724, DueDate: 1543616724, + BucketID: 3, } task7 := &Task{ ID: 7, @@ -192,6 +198,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { Created: 1543626724, Updated: 1543626724, StartDate: 1544600000, + BucketID: 3, } task8 := &Task{ ID: 8, @@ -205,6 +212,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { Created: 1543626724, Updated: 1543626724, EndDate: 1544700000, + BucketID: 3, } task9 := &Task{ ID: 9, @@ -445,6 +453,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { ListID: 1, Created: 1543626724, Updated: 1543626724, + BucketID: 1, }, }, }, -- 2.40.1