Compare commits

...

3 Commits

Author SHA1 Message Date
476bf072b1 Rebase from origin/main 2023-03-22 16:21:16 +01:00
d74334a590 feat(caldav): CALDAV subtask import support 2023-03-22 14:30:08 +01:00
c06f35e6a0 feat(caldav): CALDAV export subtask support
When exporting tasks in CALDAV, export attribute "Related-To" with the "PARENT" relationship, so that clients can display tasks with their proper hierarchy.
2023-03-22 09:53:06 +01:00
18 changed files with 807 additions and 62 deletions

1
go.mod
View File

@ -65,6 +65,7 @@ require (
github.com/wneessen/go-mail v0.3.8 github.com/wneessen/go-mail v0.3.8
github.com/yuin/goldmark v1.5.4 github.com/yuin/goldmark v1.5.4
golang.org/x/crypto v0.7.0 golang.org/x/crypto v0.7.0
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0
golang.org/x/image v0.6.0 golang.org/x/image v0.6.0
golang.org/x/oauth2 v0.6.0 golang.org/x/oauth2 v0.6.0
golang.org/x/sync v0.1.0 golang.org/x/sync v0.1.0

2
go.sum
View File

@ -782,6 +782,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=

View File

@ -51,20 +51,20 @@ type Todo struct {
UID string UID string
// Optional // Optional
Summary string Summary string
Description string Description string
Completed time.Time Completed time.Time
Organizer *user.User Organizer *user.User
Priority int64 // 0-9, 1 is highest Priority int64 // 0-9, 1 is highest
RelatedToUID string RelatedToParentUID string
Color string Color string
Categories []string Categories []string
Start time.Time Start time.Time
End time.Time End time.Time
DueDate time.Time DueDate time.Time
Duration time.Duration Duration time.Duration
RepeatAfter int64 RepeatAfter int64
RepeatMode models.TaskRepeatMode RepeatMode models.TaskRepeatMode
Created time.Time Created time.Time
Updated time.Time // last-mod Updated time.Time // last-mod
@ -209,9 +209,9 @@ STATUS:COMPLETED`
ORGANIZER;CN=:` + t.Organizer.Username ORGANIZER;CN=:` + t.Organizer.Username
} }
if t.RelatedToUID != "" { if t.RelatedToParentUID != "" {
caldavtodos += ` caldavtodos += `
RELATED-TO:` + t.RelatedToUID RELATED-TO;RELTYPE=PARENT:` + t.RelatedToParentUID
} }
if t.DueDate.Unix() > 0 { if t.DueDate.Unix() > 0 {

View File

@ -520,6 +520,45 @@ X-FUNAMBOL-COLOR:#affffeFF
CATEGORIES:label1,label2 CATEGORIES:label1,label2
LAST-MODIFIED:00010101T000000Z LAST-MODIFIED:00010101T000000Z
END:VTODO END:VTODO
END:VCALENDAR`,
},
{
name: "with parent task",
args: args{
config: &Config{
Name: "test",
ProdID: "RandomProdID which is not random",
Color: "ffffff",
},
todos: []*Todo{
{
Summary: "Todo #1",
UID: "randommduid",
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Color: "affffe",
RelatedToParentUID: "another_random_uid",
},
},
},
wantCaldavtasks: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
X-APPLE-CALENDAR-COLOR:#ffffffFF
X-OUTLOOK-COLOR:#ffffffFF
X-FUNAMBOL-COLOR:#ffffffFF
BEGIN:VTODO
UID:randommduid
DTSTAMP:20181201T011204Z
SUMMARY:Todo #1
X-APPLE-CALENDAR-COLOR:#affffeFF
X-OUTLOOK-COLOR:#affffeFF
X-FUNAMBOL-COLOR:#affffeFF
RELATED-TO;RELTYPE=PARENT:another_random_uid
LAST-MODIFIED:00010101T000000Z
END:VTODO
END:VCALENDAR`, END:VCALENDAR`,
}, },
} }

View File

@ -39,6 +39,12 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
categories = append(categories, label.Title) categories = append(categories, label.Title)
} }
// Find the UID of the parent task, if it exists:
var parentTaskUID string
if parentTasks, ok := t.RelatedTasks[models.RelationKindParenttask]; ok {
parentTaskUID = parentTasks[0].UID
}
caldavtodos = append(caldavtodos, &Todo{ caldavtodos = append(caldavtodos, &Todo{
Timestamp: t.Updated, Timestamp: t.Updated,
UID: t.UID, UID: t.UID,
@ -46,16 +52,17 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
Description: t.Description, Description: t.Description,
Completed: t.DoneAt, Completed: t.DoneAt,
// Organizer: &t.CreatedBy, // Disabled until we figure out how this works // Organizer: &t.CreatedBy, // Disabled until we figure out how this works
Categories: categories, Categories: categories,
Priority: t.Priority, Priority: t.Priority,
Start: t.StartDate, RelatedToParentUID: parentTaskUID,
End: t.EndDate, Start: t.StartDate,
Created: t.Created, End: t.EndDate,
Updated: t.Updated, Created: t.Created,
DueDate: t.DueDate, Updated: t.Updated,
Duration: duration, DueDate: t.DueDate,
RepeatAfter: t.RepeatAfter, Duration: duration,
RepeatMode: t.RepeatMode, RepeatAfter: t.RepeatAfter,
RepeatMode: t.RepeatMode,
}) })
} }
@ -73,9 +80,11 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
return nil, err return nil, err
} }
var parsedProperties = parsed.Components[0].UnknownPropertiesIANAProperties()
// We put the task details in a map to be able to handle them more easily // We put the task details in a map to be able to handle them more easily
task := make(map[string]string) task := make(map[string]string)
for _, c := range parsed.Components[0].UnknownPropertiesIANAProperties() { for _, c := range parsedProperties {
task[c.IANAToken] = c.Value task[c.IANAToken] = c.Value
} }
@ -90,6 +99,26 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
priority = parseVTODOPriority(priorityParsed) priority = parseVTODOPriority(priorityParsed)
} }
// Check if the task has a parent:
var parentTaskUID string
for _, c := range parsedProperties {
// Check if the entry is a relation:
if c.IANAToken != "RELATED-TO" {
continue
}
// Check if the relation has a type:
if _, ok := c.ICalParameters["RELTYPE"]; !ok {
continue
}
// Check that the type is "PARENT":
if len(c.ICalParameters["RELTYPE"]) != 1 || c.ICalParameters["RELTYPE"][0] != "PARENT" {
continue
}
// We have the id of the parent task:
parentTaskUID = c.Value
}
// Parse the enddate // Parse the enddate
duration, _ := time.ParseDuration(task["DURATION"]) duration, _ := time.ParseDuration(task["DURATION"])
@ -123,6 +152,19 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
vTask.Done = true vTask.Done = true
} }
// Check if the task has a parent and create a dummy relation if yes:
if parentTaskUID != "" {
var parentTaskUID = parentTaskUID
if vTask.RelatedTasks == nil {
vTask.RelatedTasks = make(models.RelatedTaskMap)
}
vTask.RelatedTasks[models.RelationKindParenttask] = append(vTask.RelatedTasks[models.RelationKindParenttask], &models.Task{
UID: parentTaskUID,
})
}
if duration > 0 && !vTask.StartDate.IsZero() { if duration > 0 && !vTask.StartDate.IsZero() {
vTask.EndDate = vTask.StartDate.Add(duration) vTask.EndDate = vTask.StartDate.Add(duration)
} }

View File

@ -118,6 +118,63 @@ END:VCALENDAR`,
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()), Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
}, },
}, },
{
name: "With parent",
args: args{content: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randomuid
DTSTAMP:20181201T011204
SUMMARY:SubTask #1
DESCRIPTION:Lorem Ipsum
RELATED-TO;RELTYPE=PARENT:randomuid_parent
LAST-MODIFIED:00010101T000000
END:VTODO
END:VCALENDAR`,
},
wantVTask: &models.Task{
Title: "SubTask #1",
UID: "randomuid",
Description: "Lorem Ipsum",
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindParenttask: {
{
UID: "randomuid_parent",
},
},
},
},
},
{
name: "With non-parent relation we ignore",
args: args{content: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randomuid
DTSTAMP:20181201T011204
SUMMARY:Parent task
DESCRIPTION:Lorem Ipsum
RELATED-TO;RELTYPE=CHILD:randomuid_child
LAST-MODIFIED:00010101T000000
END:VTODO
END:VCALENDAR`,
},
wantVTask: &models.Task{
Title: "Parent task",
UID: "randomuid",
Description: "Lorem Ipsum",
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -135,8 +192,8 @@ END:VCALENDAR`,
func TestGetCaldavTodosForTasks(t *testing.T) { func TestGetCaldavTodosForTasks(t *testing.T) {
type args struct { type args struct {
list *models.ProjectWithTasksAndBuckets project *models.ProjectWithTasksAndBuckets
tasks []*models.TaskWithComments tasks []*models.TaskWithComments
} }
tests := []struct { tests := []struct {
name string name string
@ -146,9 +203,9 @@ func TestGetCaldavTodosForTasks(t *testing.T) {
{ {
name: "Format single Task as Caldav", name: "Format single Task as Caldav",
args: args{ args: args{
list: &models.ProjectWithTasksAndBuckets{ project: &models.ProjectWithTasksAndBuckets{
Project: models.Project{ Project: models.Project{
Title: "List title", Title: "Project title",
}, },
}, },
tasks: []*models.TaskWithComments{ tasks: []*models.TaskWithComments{
@ -183,7 +240,7 @@ func TestGetCaldavTodosForTasks(t *testing.T) {
VERSION:2.0 VERSION:2.0
METHOD:PUBLISH METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:List title X-WR-CALNAME:Project title
PRODID:-//Vikunja Todo App//EN PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO BEGIN:VTODO
UID:randomuid UID:randomuid
@ -201,12 +258,126 @@ RRULE:FREQ=SECONDLY;INTERVAL=86400
CATEGORIES:label1,label2 CATEGORIES:label1,label2
LAST-MODIFIED:20181201T011205Z LAST-MODIFIED:20181201T011205Z
END:VTODO END:VTODO
END:VCALENDAR`,
},
{
name: "Format tasks with relationship as Caldav",
args: args{
project: &models.ProjectWithTasksAndBuckets{
Project: models.Project{
Title: "Project title",
},
},
tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Parent task",
UID: "randomuid_parent",
Description: "This is a parent task",
Priority: 3,
Created: time.Unix(1543626721, 0).In(config.GetTimeZone()),
Updated: time.Unix(1543626725, 0).In(config.GetTimeZone()),
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
Title: "Subtask 1",
UID: "randomuid_child_1",
Description: "This is the first child task",
Created: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
{
Title: "Subtask 2",
UID: "randomuid_child_2",
Description: "This is the second child task",
Created: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
},
},
},
{
Task: models.Task{
Title: "Subtask 1",
UID: "randomuid_child_1",
Description: "This is the first child task",
Created: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindParenttask: {
{
Title: "Parent task",
UID: "randomuid_parent",
Description: "This is a parent task",
Priority: 3,
Created: time.Unix(1543626721, 0).In(config.GetTimeZone()),
},
},
},
},
},
{
Task: models.Task{
Title: "Subtask 2",
UID: "randomuid_child_2",
Description: "This is the second child task",
Created: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindParenttask: {
{
Title: "Parent task",
UID: "randomuid_parent",
Description: "This is a parent task",
Priority: 3,
Created: time.Unix(1543626721, 0).In(config.GetTimeZone()),
},
},
},
},
},
},
},
wantCaldav: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project title
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:randomuid_parent
DTSTAMP:20181201T011205Z
SUMMARY:Parent task
DESCRIPTION:This is a parent task
CREATED:20181201T011201Z
PRIORITY:3
LAST-MODIFIED:20181201T011205Z
END:VTODO
BEGIN:VTODO
UID:randomuid_child_1
DTSTAMP:20181201T011204Z
SUMMARY:Subtask 1
DESCRIPTION:This is the first child task
RELATED-TO;RELTYPE=PARENT:randomuid_parent
CREATED:20181201T011204Z
LAST-MODIFIED:20181201T011204Z
END:VTODO
BEGIN:VTODO
UID:randomuid_child_2
DTSTAMP:20181201T011204Z
SUMMARY:Subtask 2
DESCRIPTION:This is the second child task
RELATED-TO;RELTYPE=PARENT:randomuid_parent
CREATED:20181201T011204Z
LAST-MODIFIED:20181201T011204Z
END:VTODO
END:VCALENDAR`, END:VCALENDAR`,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := GetCaldavTodosForTasks(tt.args.list, tt.args.tasks) got := GetCaldavTodosForTasks(tt.args.project, tt.args.tasks)
if diff, equal := messagediff.PrettyDiff(got, tt.wantCaldav); !equal { if diff, equal := messagediff.PrettyDiff(got, tt.wantCaldav); !equal {
t.Errorf("GetCaldavTodosForTasks() gotVTask = %v, want %v, diff = %s", got, tt.wantCaldav, diff) t.Errorf("GetCaldavTodosForTasks() gotVTask = %v, want %v, diff = %s", got, tt.wantCaldav, diff)
} }

View File

@ -224,3 +224,9 @@
created_by_id: 15 created_by_id: 15
created: 2020-04-18 21:13:52 created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52 updated: 2020-04-18 21:13:52
- id: 37
title: testbucket37
project_id: 27
created_by_id: 15
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52

View File

@ -255,7 +255,7 @@
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
- -
id: 26 id: 26
title: List 26 for Caldav tests title: Project 26 for Caldav tests
description: Lorem Ipsum description: Lorem Ipsum
identifier: test26 identifier: test26
owner_id: 15 owner_id: 15
@ -263,3 +263,14 @@
position: 1 position: 1
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
-
id: 27
title: Project 27 for Caldav tests
description: Lorem Ipsum
identifier: test27
owner_id: 15
namespace_id: 18
position: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -52,3 +52,9 @@
right: 0 right: 0
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12
- id: 10
user_id: 15
project_id: 27
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -59,7 +59,7 @@ func InitTestFileFixtures(t *testing.T) {
} }
// InitTests handles the actual bootstrapping of the test env // InitTests handles the actual bootstrapping of the test env
func InitTests() { func InitTests(loadFixtures bool) {
var err error var err error
x, err = db.CreateTestEngine() x, err = db.CreateTestEngine()
if err != nil { if err != nil {
@ -71,9 +71,11 @@ func InitTests() {
log.Fatal(err) log.Fatal(err)
} }
err = db.InitTestFixtures("files") if loadFixtures {
if err != nil { err = db.InitTestFixtures("files")
log.Fatal(err) if err != nil {
log.Fatal(err)
}
} }
InitTestFileHandler() InitTestFileHandler()

View File

@ -23,6 +23,6 @@ import (
// TestMain is the main test function used to bootstrap the test env // TestMain is the main test function used to bootstrap the test env
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
InitTests() InitTests(true)
os.Exit(m.Run()) os.Exit(m.Run())
} }

View File

@ -28,7 +28,7 @@ const vtodo = `BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
METHOD:PUBLISH METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:List 26 for Caldav tests X-WR-CALNAME:Project 26 for Caldav tests
PRODID:-//Vikunja Todo App//EN PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO BEGIN:VTODO
UID:uid UID:uid
@ -42,22 +42,22 @@ END:VCALENDAR`
func TestCaldav(t *testing.T) { func TestCaldav(t *testing.T) {
t.Run("Delivers VTODO for project", func(t *testing.T) { t.Run("Delivers VTODO for project", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "26"}) rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "26"}, true)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR") assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "PRODID:-//Vikunja Todo App//EN") assert.Contains(t, rec.Body.String(), "PRODID:-//Vikunja Todo App//EN")
assert.Contains(t, rec.Body.String(), "X-WR-CALNAME:List 26 for Caldav tests") assert.Contains(t, rec.Body.String(), "X-WR-CALNAME:Project 26 for Caldav tests")
assert.Contains(t, rec.Body.String(), "BEGIN:VTODO") assert.Contains(t, rec.Body.String(), "BEGIN:VTODO")
assert.Contains(t, rec.Body.String(), "END:VTODO") assert.Contains(t, rec.Body.String(), "END:VTODO")
assert.Contains(t, rec.Body.String(), "END:VCALENDAR") assert.Contains(t, rec.Body.String(), "END:VCALENDAR")
}) })
t.Run("Import VTODO", func(t *testing.T) { t.Run("Import VTODO", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodo, nil, map[string]string{"project": "26", "task": "uid"}) rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodo, nil, map[string]string{"project": "26", "task": "uid"}, true)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201) assert.Equal(t, rec.Result().StatusCode, 201)
}) })
t.Run("Export VTODO", func(t *testing.T) { t.Run("Export VTODO", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid-caldav-test"}) rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid-caldav-test"}, true)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR") assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "SUMMARY:Title Caldav Test") assert.Contains(t, rec.Body.String(), "SUMMARY:Title Caldav Test")
@ -67,3 +67,366 @@ func TestCaldav(t *testing.T) {
assert.Contains(t, rec.Body.String(), "CATEGORIES:Label #4") assert.Contains(t, rec.Body.String(), "CATEGORIES:Label #4")
}) })
} }
// Here we check that the CALDAV implementation correctly supports subtasks:
func TestCaldavSubtasks(t *testing.T) {
const vtodoParentTask = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 26 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_parent_task
DTSTAMP:20230301T073337Z
SUMMARY:Caldav parent task
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
END:VTODO
END:VCALENDAR`
const vtodoChildTask1 = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 26 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_child1
DTSTAMP:20230301T073337Z
SUMMARY:Caldav child task 1
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_parent_task
END:VTODO
END:VCALENDAR`
const vtodoChildTask2 = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 26 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_child2
DTSTAMP:20230301T073337Z
SUMMARY:Caldav child task 2
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_parent_task
END:VTODO
END:VCALENDAR`
const vtodoGrandChildTask = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 26 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_grand_child
DTSTAMP:20230301T073337Z
SUMMARY:Caldav grand child task
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_child1
END:VTODO
END:VCALENDAR`
t.Run("Import parent task", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoParentTask, nil, map[string]string{"project": "26", "task": "uid_parent_task"}, true)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
})
t.Run("Import children tasks", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoChildTask1, nil, map[string]string{"project": "26", "task": "uid_child1"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
rec, err = newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoChildTask2, nil, map[string]string{"project": "26", "task": "uid_child2"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
})
t.Run("Import grand child task", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoGrandChildTask, nil, map[string]string{"project": "26", "task": "uid_grand_child"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
})
t.Run("Check the relationship between all the tasks by fetching them one by one", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid_parent_task"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "UID:uid_parent_task")
assert.NotContains(t, rec.Body.String(), "RELATED-TO")
rec, err = newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid_child1"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "UID:uid_child1")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_task")
rec, err = newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid_child2"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "UID:uid_child2")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_task")
rec, err = newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid_grand_child"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "UID:uid_grand_child")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_child1")
})
const vtodoEditedGrandChildTask = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 26 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_grand_child
DTSTAMP:20230301T073337Z
SUMMARY:Caldav grand child task edited
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_child1
END:VTODO
END:VCALENDAR`
t.Run("Update the grand child task again and check that the relation is still there", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoEditedGrandChildTask, nil, map[string]string{"project": "26", "task": "uid_grand_child"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
rec, err = newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid_grand_child"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "UID:uid_grand_child")
assert.Contains(t, rec.Body.String(), "SUMMARY:Caldav grand child task edited")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_child1")
})
const vtodoChildTask2WithoutRelation = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 26 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_child2
DTSTAMP:20230301T073337Z
SUMMARY:Caldav child task 2
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
END:VTODO
END:VCALENDAR`
t.Run("Remove the relation from the second child", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoChildTask2WithoutRelation, nil, map[string]string{"project": "26", "task": "uid_child2"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
// Check that the relation was removed from the DB, and isn't returned anymore:
rec, err = newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid_child2"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "UID:uid_child2")
assert.NotContains(t, rec.Body.String(), "RELATED-TO")
})
const vtodoGrandChildTaskNewParent = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 26 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_grand_child
DTSTAMP:20230301T073337Z
SUMMARY:Caldav grand child task edited
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_parent_task
END:VTODO
END:VCALENDAR`
t.Run("Update the grand child task again and change its parent", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoGrandChildTaskNewParent, nil, map[string]string{"project": "26", "task": "uid_grand_child"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
rec, err = newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid_grand_child"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "UID:uid_grand_child")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_task")
})
}
// Here we check that the CALDAV implementation correctly supports task relations from different lists:
func TestCaldavSubtasksDifferentLists(t *testing.T) {
const vtodoParentTask = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 26 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_parent_task
DTSTAMP:20230301T073337Z
SUMMARY:Caldav parent task
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
END:VTODO
END:VCALENDAR`
const vtodoChildTask1 = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 27 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_child1
DTSTAMP:20230301T073337Z
SUMMARY:Caldav child task 1
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_parent_task
END:VTODO
END:VCALENDAR`
t.Run("Import parent task", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoParentTask, nil, map[string]string{"project": "26", "task": "uid_parent_task"}, true)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
})
t.Run("Import child tasks into a different list", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoChildTask1, nil, map[string]string{"project": "27", "task": "uid_child1"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
})
t.Run("Check the relationship between all the tasks by fetching them one by one", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid_parent_task"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "UID:uid_parent_task")
assert.NotContains(t, rec.Body.String(), "RELATED-TO")
rec, err = newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "27", "task": "uid_child1"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "UID:uid_child1")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_task")
})
}
// Here we check that subtasks are handled properly even if the children tasks are created before the parent tasks
func TestCaldavSubtasksInverseOrder(t *testing.T) {
const vtodoGrandChildTask = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 26 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_grand_child
DTSTAMP:20230301T073337Z
SUMMARY:Caldav grand child task
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_child1
END:VTODO
END:VCALENDAR`
const vtodoChildTask1 = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 26 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_child1
DTSTAMP:20230301T073337Z
SUMMARY:Caldav child task 1
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_parent_task
END:VTODO
END:VCALENDAR`
const vtodoParentTask = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 26 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_parent_task
DTSTAMP:20230301T073337Z
SUMMARY:Caldav parent task
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
END:VTODO
END:VCALENDAR`
t.Run("Import grand child task", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoGrandChildTask, nil, map[string]string{"project": "26", "task": "uid_grand_child"}, true)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
})
t.Run("Import children tasks", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoChildTask1, nil, map[string]string{"project": "26", "task": "uid_child1"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
})
t.Run("Import parent task", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoParentTask, nil, map[string]string{"project": "26", "task": "uid_parent_task"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
})
t.Run("Check the relationship between all the tasks by fetching them one by one", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid_parent_task"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "UID:uid_parent_task")
assert.Contains(t, rec.Body.String(), "SUMMARY:Caldav parent task")
assert.NotContains(t, rec.Body.String(), "RELATED-TO")
rec, err = newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid_child1"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "UID:uid_child1")
assert.Contains(t, rec.Body.String(), "SUMMARY:Caldav child task 1")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_task")
rec, err = newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "26", "task": "uid_grand_child"}, false)
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "UID:uid_grand_child")
assert.Contains(t, rec.Body.String(), "SUMMARY:Caldav grand child task")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_child1")
})
}

View File

@ -58,20 +58,22 @@ var (
} }
) )
func setupTestEnv() (e *echo.Echo, err error) { func setupTestEnv(loadFixtures bool) (e *echo.Echo, err error) {
config.InitDefaultConfig() config.InitDefaultConfig()
// We need to set the root path even if we're not using the config, otherwise fixtures are not loaded correctly // We need to set the root path even if we're not using the config, otherwise fixtures are not loaded correctly
config.ServiceRootpath.Set(os.Getenv("VIKUNJA_SERVICE_ROOTPATH")) config.ServiceRootpath.Set(os.Getenv("VIKUNJA_SERVICE_ROOTPATH"))
// Some tests use the file engine, so we'll need to initialize that // Some tests use the file engine, so we'll need to initialize that
files.InitTests() files.InitTests(loadFixtures)
user.InitTests() user.InitTests()
models.SetupTests() models.SetupTests()
events.Fake() events.Fake()
keyvalue.InitStorage() keyvalue.InitStorage()
err = db.LoadFixtures() if loadFixtures {
if err != nil { err = db.LoadFixtures()
return if err != nil {
return
}
} }
e = routes.NewEcho() e = routes.NewEcho()
@ -79,9 +81,9 @@ func setupTestEnv() (e *echo.Echo, err error) {
return return
} }
func bootstrapTestRequest(t *testing.T, method string, payload string, queryParam url.Values) (c echo.Context, rec *httptest.ResponseRecorder) { func bootstrapTestRequest(t *testing.T, method string, payload string, queryParam url.Values, loadFixtures bool) (c echo.Context, rec *httptest.ResponseRecorder) {
// Setup // Setup
e, err := setupTestEnv() e, err := setupTestEnv(loadFixtures)
assert.NoError(t, err) assert.NoError(t, err)
// Do the actual request // Do the actual request
@ -95,7 +97,7 @@ func bootstrapTestRequest(t *testing.T, method string, payload string, queryPara
} }
func newTestRequest(t *testing.T, method string, handler func(ctx echo.Context) error, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { func newTestRequest(t *testing.T, method string, handler func(ctx echo.Context) error, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams) rec, c := testRequestSetup(t, method, payload, queryParams, urlParams, true)
err = handler(c) err = handler(c)
return return
} }
@ -124,8 +126,8 @@ func addLinkShareTokenToContext(t *testing.T, share *models.LinkSharing, c echo.
c.Set("user", tken) c.Set("user", tken)
} }
func testRequestSetup(t *testing.T, method string, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, c echo.Context) { func testRequestSetup(t *testing.T, method string, payload string, queryParams url.Values, urlParams map[string]string, loadFixtures bool) (rec *httptest.ResponseRecorder, c echo.Context) {
c, rec = bootstrapTestRequest(t, method, payload, queryParams) c, rec = bootstrapTestRequest(t, method, payload, queryParams, loadFixtures)
var paramNames []string var paramNames []string
var paramValues []string var paramValues []string
@ -139,21 +141,21 @@ func testRequestSetup(t *testing.T, method string, payload string, queryParams u
} }
func newTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { func newTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams) rec, c := testRequestSetup(t, method, payload, queryParams, urlParams, true)
addUserTokenToContext(t, user, c) addUserTokenToContext(t, user, c)
err = handler(c) err = handler(c)
return return
} }
func newTestRequestWithLinkShare(t *testing.T, method string, handler echo.HandlerFunc, share *models.LinkSharing, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { func newTestRequestWithLinkShare(t *testing.T, method string, handler echo.HandlerFunc, share *models.LinkSharing, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams) rec, c := testRequestSetup(t, method, payload, queryParams, urlParams, true)
addLinkShareTokenToContext(t, share, c) addLinkShareTokenToContext(t, share, c)
err = handler(c) err = handler(c)
return return
} }
func newCaldavTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { func newCaldavTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string, loadFixtures bool) (rec *httptest.ResponseRecorder, err error) {
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams) rec, c := testRequestSetup(t, method, payload, queryParams, urlParams, loadFixtures)
c.Request().Header.Set(echo.HeaderContentType, echo.MIMETextPlain) c.Request().Header.Set(echo.HeaderContentType, echo.MIMETextPlain)
result, _ := caldav.BasicAuth(user.Username, "1234", c) result, _ := caldav.BasicAuth(user.Username, "1234", c)

View File

@ -59,7 +59,7 @@ func TestMain(m *testing.M) {
config.ServiceRootpath.Set(os.Getenv("VIKUNJA_SERVICE_ROOTPATH")) config.ServiceRootpath.Set(os.Getenv("VIKUNJA_SERVICE_ROOTPATH"))
// Some tests use the file engine, so we'll need to initialize that // Some tests use the file engine, so we'll need to initialize that
files.InitTests() files.InitTests(true)
user.InitTests() user.InitTests()

View File

@ -30,7 +30,7 @@ import (
// TestMain is the main test function used to bootstrap the test env // TestMain is the main test function used to bootstrap the test env
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
user.InitTests() user.InitTests()
files.InitTests() files.InitTests(true)
models.SetupTests() models.SetupTests()
events.Fake() events.Fake()
os.Exit(m.Run()) os.Exit(m.Run())

View File

@ -36,7 +36,7 @@ func TestMain(m *testing.M) {
config.ServiceRootpath.Set(os.Getenv("VIKUNJA_SERVICE_ROOTPATH")) config.ServiceRootpath.Set(os.Getenv("VIKUNJA_SERVICE_ROOTPATH"))
// Some tests use the file engine, so we'll need to initialize that // Some tests use the file engine, so we'll need to initialize that
files.InitTests() files.InitTests(true)
user.InitTests() user.InitTests()
models.SetupTests() models.SetupTests()
events.Fake() events.Fake()

View File

@ -36,7 +36,7 @@ func TestMain(m *testing.M) {
config.ServiceRootpath.Set(os.Getenv("VIKUNJA_SERVICE_ROOTPATH")) config.ServiceRootpath.Set(os.Getenv("VIKUNJA_SERVICE_ROOTPATH"))
// Some tests use the file engine, so we'll need to initialize that // Some tests use the file engine, so we'll need to initialize that
files.InitTests() files.InitTests(true)
user.InitTests() user.InitTests()
models.SetupTests() models.SetupTests()
events.Fake() events.Fake()

View File

@ -17,6 +17,7 @@
package caldav package caldav
import ( import (
"golang.org/x/exp/slices"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -294,6 +295,13 @@ func (vcls *VikunjaCaldavProjectStorage) CreateResource(rpath, content string) (
return nil, err return nil, err
} }
vcls.task.ProjectID = vcls.project.ID
err = persistRelations(s, vcls.user, vcls.task, vTask.RelatedTasks)
if err != nil {
_ = s.Rollback()
return nil, err
}
if err := s.Commit(); err != nil { if err := s.Commit(); err != nil {
return nil, err return nil, err
} }
@ -318,6 +326,10 @@ func (vcls *VikunjaCaldavProjectStorage) UpdateResource(rpath, content string) (
// At this point, we already have the right task in vcls.task, so we can use that ID directly // At this point, we already have the right task in vcls.task, so we can use that ID directly
vTask.ID = vcls.task.ID vTask.ID = vcls.task.ID
// Explicitely set the ProjectID in case the task now belongs to a different project:
vTask.ProjectID = vcls.project.ID
vcls.task.ProjectID = vcls.project.ID
s := db.NewSession() s := db.NewSession()
defer s.Close() defer s.Close()
@ -345,6 +357,12 @@ func (vcls *VikunjaCaldavProjectStorage) UpdateResource(rpath, content string) (
return nil, err return nil, err
} }
err = persistRelations(s, vcls.user, vcls.task, vTask.RelatedTasks)
if err != nil {
_ = s.Rollback()
return nil, err
}
if err := s.Commit(); err != nil { if err := s.Commit(); err != nil {
return nil, err return nil, err
} }
@ -432,6 +450,88 @@ func persistLabels(s *xorm.Session, a web.Auth, task *models.Task, labels []*mod
return task.UpdateTaskLabels(s, a, labels) return task.UpdateTaskLabels(s, a, labels)
} }
// When a VTODO entry doesn't have a parent anymore, but we do, we need to remove it as well.
func removeLegacyParentRelations(s *xorm.Session, a web.Auth, task *models.Task, newRelations map[models.RelationKind][]*models.Task) (err error) {
// Get the existing task with details:
existingTask := &models.Task{ID: task.ID}
err = existingTask.ReadOne(s, a)
if err != nil {
return
}
// Loop through all the existing task's parent relationships:
if _, ok := existingTask.RelatedTasks[models.RelationKindParenttask]; ok {
for _, parentTask := range existingTask.RelatedTasks[models.RelationKindParenttask] {
// Check if the existing parent relation is in the new list:
parentRelationInNewList := slices.ContainsFunc(newRelations[models.RelationKindParenttask], func(newRelation *models.Task) bool { return newRelation.UID == parentTask.UID })
// Remove the relations if it's not there in the new list anymore:
if !parentRelationInNewList {
rel := models.TaskRelation{
TaskID: task.ID,
OtherTaskID: parentTask.ID,
RelationKind: models.RelationKindParenttask,
}
err = rel.Delete(s, a)
if err != nil {
return
}
}
}
}
return
}
// Persist new relations provided by the VTODO entry:
func persistRelations(s *xorm.Session, a web.Auth, task *models.Task, newRelations map[models.RelationKind][]*models.Task) (err error) {
// Remove existing "parent" relations that are not present in the new list:
err = removeLegacyParentRelations(s, a, task, newRelations)
if err != nil {
return err
}
// Ensure the current relations exist:
for relationType, relatedTasks := range newRelations {
// Persist each relation independently:
for _, relatedTask := range relatedTasks {
// Get the task from the DB:
has, err := s.Get(relatedTask)
if err != nil {
return err
}
// If the related task doesn't exist, create a dummy one now in the same list.
// It'll probably be populated right after in a following request:
if !has {
relatedTask.ProjectID = task.ProjectID
relatedTask.Title = "UID-" + relatedTask.UID
err = relatedTask.Create(s, a)
if err != nil {
return err
}
}
// Create the relation:
rel := models.TaskRelation{
TaskID: task.ID,
OtherTaskID: relatedTask.ID,
RelationKind: relationType,
}
err = rel.Create(s, a)
if err != nil && !models.IsErrRelationAlreadyExists(err) {
return err
}
}
}
return err
}
// VikunjaProjectResourceAdapter holds the actual resource // VikunjaProjectResourceAdapter holds the actual resource
type VikunjaProjectResourceAdapter struct { type VikunjaProjectResourceAdapter struct {
project *models.ProjectWithTasksAndBuckets project *models.ProjectWithTasksAndBuckets