feat(caldav): Add support for subtasks (i.e. RELATED-TO
property) in CalDAV (#1634)
As I mentioned [here](https://kolaente.dev/vikunja/api/pulls/1442#issuecomment-55215), this is mainly a cleanup of @zewaren 's original [PR](https://kolaente.dev/vikunja/api/pulls/1442). It adds support for the `RELATED-TO` property in CalDAV's `VTODO` and the `RELTYPE=PARENT` and `RELTYPE=CHILD` relationships. In other words, it allows for `ParentTask->SubTask` relations to be handled supported through CalDAV. In addition to the included tests, this has been tested by both @zewaren & myself with DAVx5 & Tasks (Android) and it's been working great. Resolves https://kolaente.dev/vikunja/api/issues/1345 Co-authored-by: Miguel A. Arroyo <miguel@codeheads.dev> Co-authored-by: Erwan Martin <public@fzwte.net> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/1634 Reviewed-by: konrad <k@knt.li> Co-authored-by: Miguel Arroyo <mayanez@noreply.kolaente.de> Co-committed-by: Miguel Arroyo <mayanez@noreply.kolaente.de>
This commit is contained in:
parent
6169c2e12e
commit
225d65268d
@ -43,7 +43,7 @@ type Todo struct {
|
|||||||
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
|
Relations []Relation
|
||||||
Color string
|
Color string
|
||||||
Categories []string
|
Categories []string
|
||||||
Start time.Time
|
Start time.Time
|
||||||
@ -66,6 +66,11 @@ type Alarm struct {
|
|||||||
Description string
|
Description string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Relation struct {
|
||||||
|
Type models.RelationKind
|
||||||
|
UID string
|
||||||
|
}
|
||||||
|
|
||||||
// Config is the caldav calendar config
|
// Config is the caldav calendar config
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Name string
|
Name string
|
||||||
@ -147,11 +152,6 @@ STATUS:COMPLETED`
|
|||||||
ORGANIZER;CN=:` + t.Organizer.Username
|
ORGANIZER;CN=:` + t.Organizer.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.RelatedToUID != "" {
|
|
||||||
caldavtodos += `
|
|
||||||
RELATED-TO:` + t.RelatedToUID
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.DueDate.Unix() > 0 {
|
if t.DueDate.Unix() > 0 {
|
||||||
caldavtodos += `
|
caldavtodos += `
|
||||||
DUE:` + makeCalDavTimeFromTimeStamp(t.DueDate)
|
DUE:` + makeCalDavTimeFromTimeStamp(t.DueDate)
|
||||||
@ -185,6 +185,7 @@ CATEGORIES:` + strings.Join(t.Categories, ",")
|
|||||||
caldavtodos += `
|
caldavtodos += `
|
||||||
LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated)
|
LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated)
|
||||||
caldavtodos += ParseAlarms(t.Alarms, t.Summary)
|
caldavtodos += ParseAlarms(t.Alarms, t.Summary)
|
||||||
|
caldavtodos += ParseRelations(t.Relations)
|
||||||
caldavtodos += `
|
caldavtodos += `
|
||||||
END:VTODO`
|
END:VTODO`
|
||||||
}
|
}
|
||||||
@ -222,6 +223,47 @@ END:VALARM`
|
|||||||
return caldavalarms
|
return caldavalarms
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseRelations(relations []Relation) (caldavrelatedtos string) {
|
||||||
|
|
||||||
|
for _, r := range relations {
|
||||||
|
switch r.Type {
|
||||||
|
case models.RelationKindParenttask:
|
||||||
|
caldavrelatedtos += `
|
||||||
|
RELATED-TO;RELTYPE=PARENT:`
|
||||||
|
case models.RelationKindSubtask:
|
||||||
|
caldavrelatedtos += `
|
||||||
|
RELATED-TO;RELTYPE=CHILD:`
|
||||||
|
case models.RelationKindUnknown:
|
||||||
|
continue
|
||||||
|
case models.RelationKindRelated:
|
||||||
|
continue
|
||||||
|
case models.RelationKindDuplicateOf:
|
||||||
|
continue
|
||||||
|
case models.RelationKindDuplicates:
|
||||||
|
continue
|
||||||
|
case models.RelationKindBlocking:
|
||||||
|
continue
|
||||||
|
case models.RelationKindBlocked:
|
||||||
|
continue
|
||||||
|
case models.RelationKindPreceeds:
|
||||||
|
continue
|
||||||
|
case models.RelationKindFollows:
|
||||||
|
continue
|
||||||
|
case models.RelationKindCopiedFrom:
|
||||||
|
continue
|
||||||
|
case models.RelationKindCopiedTo:
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
caldavrelatedtos += `
|
||||||
|
RELATED-TO:`
|
||||||
|
}
|
||||||
|
|
||||||
|
caldavrelatedtos += r.UID
|
||||||
|
}
|
||||||
|
|
||||||
|
return caldavrelatedtos
|
||||||
|
}
|
||||||
|
|
||||||
func makeCalDavTimeFromTimeStamp(ts time.Time) (caldavtime string) {
|
func makeCalDavTimeFromTimeStamp(ts time.Time) (caldavtime string) {
|
||||||
return ts.In(time.UTC).Format(DateFormat) + "Z"
|
return ts.In(time.UTC).Format(DateFormat) + "Z"
|
||||||
}
|
}
|
||||||
|
@ -326,6 +326,49 @@ ACTION:DISPLAY
|
|||||||
DESCRIPTION:Todo #1
|
DESCRIPTION:Todo #1
|
||||||
END:VALARM
|
END:VALARM
|
||||||
END:VTODO
|
END:VTODO
|
||||||
|
END:VCALENDAR`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with related-to",
|
||||||
|
args: args{
|
||||||
|
config: &Config{
|
||||||
|
Name: "test",
|
||||||
|
ProdID: "RandomProdID which is not random",
|
||||||
|
},
|
||||||
|
todos: []*Todo{
|
||||||
|
{
|
||||||
|
Summary: "Todo #1",
|
||||||
|
Description: "Lorem Ipsum",
|
||||||
|
UID: "randommduid",
|
||||||
|
Relations: []Relation{
|
||||||
|
{
|
||||||
|
Type: models.RelationKindParenttask,
|
||||||
|
UID: "parentuid",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: models.RelationKindSubtask,
|
||||||
|
UID: "subtaskuid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantCaldavtasks: `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:randommduid
|
||||||
|
DTSTAMP:20181201T011204Z
|
||||||
|
SUMMARY:Todo #1
|
||||||
|
DESCRIPTION:Lorem Ipsum
|
||||||
|
LAST-MODIFIED:00010101T000000Z
|
||||||
|
RELATED-TO;RELTYPE=PARENT:parentuid
|
||||||
|
RELATED-TO;RELTYPE=CHILD:subtaskuid
|
||||||
|
END:VTODO
|
||||||
END:VCALENDAR`,
|
END:VCALENDAR`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/config"
|
"code.vikunja.io/api/pkg/config"
|
||||||
"code.vikunja.io/api/pkg/db"
|
|
||||||
"code.vikunja.io/api/pkg/log"
|
"code.vikunja.io/api/pkg/log"
|
||||||
"code.vikunja.io/api/pkg/models"
|
"code.vikunja.io/api/pkg/models"
|
||||||
"code.vikunja.io/api/pkg/utils"
|
"code.vikunja.io/api/pkg/utils"
|
||||||
@ -51,6 +50,16 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var relations []Relation
|
||||||
|
for reltype, tasks := range t.RelatedTasks {
|
||||||
|
for _, r := range tasks {
|
||||||
|
relations = append(relations, Relation{
|
||||||
|
Type: reltype,
|
||||||
|
UID: r.UID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
caldavtodos = append(caldavtodos, &Todo{
|
caldavtodos = append(caldavtodos, &Todo{
|
||||||
Timestamp: t.Updated,
|
Timestamp: t.Updated,
|
||||||
UID: t.UID,
|
UID: t.UID,
|
||||||
@ -69,6 +78,7 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
|
|||||||
RepeatAfter: t.RepeatAfter,
|
RepeatAfter: t.RepeatAfter,
|
||||||
RepeatMode: t.RepeatMode,
|
RepeatMode: t.RepeatMode,
|
||||||
Alarms: alarms,
|
Alarms: alarms,
|
||||||
|
Relations: relations,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,11 +101,11 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||||||
}
|
}
|
||||||
// We put the vTodo details in a map to be able to handle them more easily
|
// We put the vTodo details in a map to be able to handle them more easily
|
||||||
task := make(map[string]ics.IANAProperty)
|
task := make(map[string]ics.IANAProperty)
|
||||||
var relation ics.IANAProperty
|
var relations []ics.IANAProperty
|
||||||
for _, c := range vTodo.UnknownPropertiesIANAProperties() {
|
for _, c := range vTodo.UnknownPropertiesIANAProperties() {
|
||||||
task[c.IANAToken] = c
|
task[c.IANAToken] = c
|
||||||
if strings.HasPrefix(c.IANAToken, "RELATED-TO") {
|
if strings.HasPrefix(c.IANAToken, "RELATED-TO") {
|
||||||
relation = c
|
relations = append(relations, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,17 +149,33 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||||||
DoneAt: caldavTimeToTimestamp(task["COMPLETED"]),
|
DoneAt: caldavTimeToTimestamp(task["COMPLETED"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
if relation.Value != "" {
|
for _, c := range relations {
|
||||||
s := db.NewSession()
|
var relTypeStr string
|
||||||
defer s.Close()
|
if _, ok := c.ICalParameters["RELTYPE"]; ok {
|
||||||
|
if len(c.ICalParameters["RELTYPE"]) != 1 {
|
||||||
subtask, err := models.GetTaskSimpleByUUID(s, relation.Value)
|
continue
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relTypeStr = c.ICalParameters["RELTYPE"][0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var relationKind models.RelationKind
|
||||||
|
switch relTypeStr {
|
||||||
|
case "PARENT":
|
||||||
|
relationKind = models.RelationKindParenttask
|
||||||
|
case "CHILD":
|
||||||
|
relationKind = models.RelationKindSubtask
|
||||||
|
default:
|
||||||
|
relationKind = models.RelationKindParenttask
|
||||||
|
}
|
||||||
|
|
||||||
|
if vTask.RelatedTasks == nil {
|
||||||
vTask.RelatedTasks = make(map[models.RelationKind][]*models.Task)
|
vTask.RelatedTasks = make(map[models.RelationKind][]*models.Task)
|
||||||
vTask.RelatedTasks[models.RelationKindSubtask] = []*models.Task{subtask}
|
}
|
||||||
|
|
||||||
|
vTask.RelatedTasks[relationKind] = append(vTask.RelatedTasks[relationKind], &models.Task{
|
||||||
|
UID: c.Value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if task["STATUS"].Value == "COMPLETED" {
|
if task["STATUS"].Value == "COMPLETED" {
|
||||||
|
@ -219,6 +219,70 @@ 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
|
||||||
|
LAST-MODIFIED:00010101T000000
|
||||||
|
RELATED-TO;RELTYPE=PARENT:randomuid_parent
|
||||||
|
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 subtask",
|
||||||
|
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
|
||||||
|
DESCRIPTION:Lorem Ipsum
|
||||||
|
LAST-MODIFIED:00010101T000000
|
||||||
|
RELATED-TO;RELTYPE=CHILD:randomuid_child
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR`,
|
||||||
|
},
|
||||||
|
wantVTask: &models.Task{
|
||||||
|
Title: "Parent",
|
||||||
|
UID: "randomuid",
|
||||||
|
Description: "Lorem Ipsum",
|
||||||
|
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||||
|
RelatedTasks: map[models.RelationKind][]*models.Task{
|
||||||
|
models.RelationKindSubtask: {
|
||||||
|
{
|
||||||
|
UID: "randomuid_child",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "example task from tasks.org app",
|
name: "example task from tasks.org app",
|
||||||
args: args{content: `BEGIN:VCALENDAR
|
args: args{content: `BEGIN:VCALENDAR
|
||||||
@ -392,6 +456,124 @@ ACTION:DISPLAY
|
|||||||
DESCRIPTION:Task 1
|
DESCRIPTION:Task 1
|
||||||
END:VALARM
|
END:VALARM
|
||||||
END:VTODO
|
END:VTODO
|
||||||
|
END:VCALENDAR`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Format Task with Related Tasks as CalDAV",
|
||||||
|
args: args{
|
||||||
|
list: &models.ProjectWithTasksAndBuckets{
|
||||||
|
Project: models.Project{
|
||||||
|
Title: "List title",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tasks: []*models.TaskWithComments{
|
||||||
|
{
|
||||||
|
Task: models.Task{
|
||||||
|
Title: "Parent Task",
|
||||||
|
UID: "randomuid_parent",
|
||||||
|
Description: "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: "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: "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: "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: "A parent task",
|
||||||
|
Priority: 3,
|
||||||
|
Created: time.Unix(1543626721, 0).In(config.GetTimeZone()),
|
||||||
|
Updated: time.Unix(1543626725, 0).In(config.GetTimeZone()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Task: models.Task{
|
||||||
|
Title: "Subtask 2",
|
||||||
|
UID: "randomuid_child_2",
|
||||||
|
Description: "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: "A parent task",
|
||||||
|
Priority: 3,
|
||||||
|
Created: time.Unix(1543626721, 0).In(config.GetTimeZone()),
|
||||||
|
Updated: time.Unix(1543626725, 0).In(config.GetTimeZone()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantCaldav: `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
X-PUBLISHED-TTL:PT4H
|
||||||
|
X-WR-CALNAME:List title
|
||||||
|
PRODID:-//Vikunja Todo App//EN
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:randomuid_parent
|
||||||
|
DTSTAMP:20181201T011205Z
|
||||||
|
SUMMARY:Parent Task
|
||||||
|
DESCRIPTION:A parent task
|
||||||
|
CREATED:20181201T011201Z
|
||||||
|
PRIORITY:3
|
||||||
|
LAST-MODIFIED:20181201T011205Z
|
||||||
|
RELATED-TO;RELTYPE=CHILD:randomuid_child_1
|
||||||
|
RELATED-TO;RELTYPE=CHILD:randomuid_child_2
|
||||||
|
END:VTODO
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:randomuid_child_1
|
||||||
|
DTSTAMP:20181201T011204Z
|
||||||
|
SUMMARY:Subtask 1
|
||||||
|
DESCRIPTION:The first child task
|
||||||
|
CREATED:20181201T011204Z
|
||||||
|
LAST-MODIFIED:20181201T011204Z
|
||||||
|
RELATED-TO;RELTYPE=PARENT:randomuid_parent
|
||||||
|
END:VTODO
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:randomuid_child_2
|
||||||
|
DTSTAMP:20181201T011204Z
|
||||||
|
SUMMARY:Subtask 2
|
||||||
|
DESCRIPTION:The second child task
|
||||||
|
CREATED:20181201T011204Z
|
||||||
|
LAST-MODIFIED:20181201T011204Z
|
||||||
|
RELATED-TO;RELTYPE=PARENT:randomuid_parent
|
||||||
|
END:VTODO
|
||||||
END:VCALENDAR`,
|
END:VCALENDAR`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -236,3 +236,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: 39
|
||||||
|
title: testbucket38
|
||||||
|
project_id: 38
|
||||||
|
created_by_id: 15
|
||||||
|
created: 2020-04-18 21:13:52
|
||||||
|
updated: 2020-04-18 21:13:52
|
@ -327,3 +327,12 @@
|
|||||||
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: 38
|
||||||
|
title: Project 38 for Caldav tests
|
||||||
|
description: Lorem Ipsum
|
||||||
|
identifier: test38
|
||||||
|
owner_id: 15
|
||||||
|
position: 2
|
||||||
|
updated: 2018-12-02 15:13:12
|
||||||
|
created: 2018-12-01 15:13:12
|
@ -34,3 +34,39 @@
|
|||||||
relation_kind: 'related'
|
relation_kind: 'related'
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
|
- id: 7
|
||||||
|
task_id: 41
|
||||||
|
other_task_id: 43
|
||||||
|
relation_kind: 'subtask'
|
||||||
|
created_by_id: 15
|
||||||
|
created: 2018-12-01 15:13:12
|
||||||
|
- id: 8
|
||||||
|
task_id: 43
|
||||||
|
other_task_id: 41
|
||||||
|
relation_kind: 'parenttask'
|
||||||
|
created_by_id: 15
|
||||||
|
created: 2018-12-01 15:13:12
|
||||||
|
- id: 9
|
||||||
|
task_id: 41
|
||||||
|
other_task_id: 44
|
||||||
|
relation_kind: 'subtask'
|
||||||
|
created_by_id: 15
|
||||||
|
created: 2018-12-01 15:13:12
|
||||||
|
- id: 10
|
||||||
|
task_id: 44
|
||||||
|
other_task_id: 41
|
||||||
|
relation_kind: 'parenttask'
|
||||||
|
created_by_id: 15
|
||||||
|
created: 2018-12-01 15:13:12
|
||||||
|
- id: 11
|
||||||
|
task_id: 45
|
||||||
|
other_task_id: 46
|
||||||
|
relation_kind: 'subtask'
|
||||||
|
created_by_id: 15
|
||||||
|
created: 2018-12-01 15:13:12
|
||||||
|
- id: 12
|
||||||
|
task_id: 46
|
||||||
|
other_task_id: 45
|
||||||
|
relation_kind: 'parenttask'
|
||||||
|
created_by_id: 15
|
||||||
|
created: 2018-12-01 15:13:12
|
@ -374,5 +374,89 @@
|
|||||||
due_date: 2023-03-01 15:00:00
|
due_date: 2023-03-01 15:00:00
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
bucket_id: 1
|
bucket_id: 38
|
||||||
position: 39
|
position: 39
|
||||||
|
- id: 41
|
||||||
|
uid: 'uid-caldav-test-parent-task'
|
||||||
|
title: 'Parent task for Caldav Test'
|
||||||
|
description: 'Description Caldav Test'
|
||||||
|
priority: 3
|
||||||
|
done: false
|
||||||
|
created_by_id: 15
|
||||||
|
project_id: 36
|
||||||
|
index: 40
|
||||||
|
due_date: 2023-03-01 15:00:00
|
||||||
|
created: 2018-12-01 01:12:04
|
||||||
|
updated: 2018-12-01 01:12:04
|
||||||
|
bucket_id: 38
|
||||||
|
position: 40
|
||||||
|
- id: 42
|
||||||
|
uid: 'uid-caldav-test-parent-task-2'
|
||||||
|
title: 'Parent task for Caldav Test 2'
|
||||||
|
description: 'Description Caldav Test 2'
|
||||||
|
priority: 3
|
||||||
|
done: false
|
||||||
|
created_by_id: 15
|
||||||
|
project_id: 36
|
||||||
|
index: 41
|
||||||
|
due_date: 2023-03-01 15:00:00
|
||||||
|
created: 2018-12-01 01:12:04
|
||||||
|
updated: 2018-12-01 01:12:04
|
||||||
|
bucket_id: 38
|
||||||
|
position: 41
|
||||||
|
- id: 43
|
||||||
|
uid: 'uid-caldav-test-child-task'
|
||||||
|
title: 'Child task for Caldav Test'
|
||||||
|
description: 'Description Caldav Test'
|
||||||
|
priority: 3
|
||||||
|
done: false
|
||||||
|
created_by_id: 15
|
||||||
|
project_id: 36
|
||||||
|
index: 42
|
||||||
|
due_date: 2023-03-01 15:00:00
|
||||||
|
created: 2018-12-01 01:12:04
|
||||||
|
updated: 2018-12-01 01:12:04
|
||||||
|
bucket_id: 38
|
||||||
|
position: 42
|
||||||
|
- id: 44
|
||||||
|
uid: 'uid-caldav-test-child-task-2'
|
||||||
|
title: 'Child task for Caldav Test '
|
||||||
|
description: 'Description Caldav Test'
|
||||||
|
priority: 3
|
||||||
|
done: false
|
||||||
|
created_by_id: 15
|
||||||
|
project_id: 38
|
||||||
|
index: 43
|
||||||
|
due_date: 2023-03-01 15:00:00
|
||||||
|
created: 2018-12-01 01:12:04
|
||||||
|
updated: 2018-12-01 01:12:04
|
||||||
|
bucket_id: 38
|
||||||
|
position: 43
|
||||||
|
- id: 45
|
||||||
|
uid: 'uid-caldav-test-parent-task-another-list'
|
||||||
|
title: 'Parent task for Caldav Test'
|
||||||
|
description: 'Description Caldav Test'
|
||||||
|
priority: 3
|
||||||
|
done: false
|
||||||
|
created_by_id: 15
|
||||||
|
project_id: 36
|
||||||
|
index: 44
|
||||||
|
due_date: 2023-03-01 15:00:00
|
||||||
|
created: 2018-12-01 01:12:04
|
||||||
|
updated: 2018-12-01 01:12:04
|
||||||
|
bucket_id: 38
|
||||||
|
position: 44
|
||||||
|
- id: 46
|
||||||
|
uid: 'uid-caldav-test-child-task-another-list'
|
||||||
|
title: 'Child task for Caldav Test '
|
||||||
|
description: 'Description Caldav Test'
|
||||||
|
priority: 3
|
||||||
|
done: false
|
||||||
|
created_by_id: 15
|
||||||
|
project_id: 38
|
||||||
|
index: 45
|
||||||
|
due_date: 2023-03-01 15:00:00
|
||||||
|
created: 2018-12-01 01:12:04
|
||||||
|
updated: 2018-12-01 01:12:04
|
||||||
|
bucket_id: 38
|
||||||
|
position: 45
|
@ -100,3 +100,15 @@
|
|||||||
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: 18
|
||||||
|
user_id: 15
|
||||||
|
project_id: 36
|
||||||
|
right: 0
|
||||||
|
updated: 2018-12-02 15:13:12
|
||||||
|
created: 2018-12-01 15:13:12
|
||||||
|
- id: 19
|
||||||
|
user_id: 15
|
||||||
|
project_id: 38
|
||||||
|
right: 0
|
||||||
|
updated: 2018-12-02 15:13:12
|
||||||
|
created: 2018-12-01 15:13:12
|
@ -24,6 +24,19 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestCaldav(t *testing.T) {
|
||||||
|
t.Run("Delivers VTODO for project", func(t *testing.T) {
|
||||||
|
e, _ := setupTestEnv()
|
||||||
|
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
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(), "X-WR-CALNAME:Project 36 for Caldav tests")
|
||||||
|
assert.Contains(t, rec.Body.String(), "BEGIN:VTODO")
|
||||||
|
assert.Contains(t, rec.Body.String(), "END:VTODO")
|
||||||
|
assert.Contains(t, rec.Body.String(), "END:VCALENDAR")
|
||||||
|
})
|
||||||
|
t.Run("Import VTODO", func(t *testing.T) {
|
||||||
const vtodo = `BEGIN:VCALENDAR
|
const vtodo = `BEGIN:VCALENDAR
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
METHOD:PUBLISH
|
METHOD:PUBLISH
|
||||||
@ -44,24 +57,14 @@ END:VALARM
|
|||||||
END:VTODO
|
END:VTODO
|
||||||
END:VCALENDAR`
|
END:VCALENDAR`
|
||||||
|
|
||||||
func TestCaldav(t *testing.T) {
|
e, _ := setupTestEnv()
|
||||||
t.Run("Delivers VTODO for project", func(t *testing.T) {
|
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, vtodo, nil, map[string]string{"project": "36", "task": "uid"})
|
||||||
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
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(), "X-WR-CALNAME:Project 36 for Caldav tests")
|
|
||||||
assert.Contains(t, rec.Body.String(), "BEGIN:VTODO")
|
|
||||||
assert.Contains(t, rec.Body.String(), "END:VTODO")
|
|
||||||
assert.Contains(t, rec.Body.String(), "END:VCALENDAR")
|
|
||||||
})
|
|
||||||
t.Run("Import VTODO", func(t *testing.T) {
|
|
||||||
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodo, nil, map[string]string{"project": "36", "task": "uid"})
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 201, rec.Result().StatusCode)
|
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||||
})
|
})
|
||||||
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": "36", "task": "uid-caldav-test"})
|
e, _ := setupTestEnv()
|
||||||
|
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test"})
|
||||||
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")
|
||||||
@ -75,3 +78,241 @@ func TestCaldav(t *testing.T) {
|
|||||||
assert.Contains(t, rec.Body.String(), "END:VALARM")
|
assert.Contains(t, rec.Body.String(), "END:VALARM")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCaldavSubtasks(t *testing.T) {
|
||||||
|
const vtodoHeader = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
X-PUBLISHED-TTL:PT4H
|
||||||
|
X-WR-CALNAME:Project 36 for Caldav tests
|
||||||
|
PRODID:-//Vikunja Todo App//EN
|
||||||
|
`
|
||||||
|
const vtodoFooter = `
|
||||||
|
END:VCALENDAR`
|
||||||
|
|
||||||
|
t.Run("Import Task & Subtask", func(t *testing.T) {
|
||||||
|
|
||||||
|
const vtodoParentTaskStub = `BEGIN:VTODO
|
||||||
|
UID:uid_parent_import
|
||||||
|
DTSTAMP:20230301T073337Z
|
||||||
|
SUMMARY:Caldav parent task
|
||||||
|
CREATED:20230301T073337Z
|
||||||
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
END:VTODO`
|
||||||
|
|
||||||
|
const vtodoChildTaskStub = `BEGIN:VTODO
|
||||||
|
UID:uid_child_import
|
||||||
|
DTSTAMP:20230301T073337Z
|
||||||
|
SUMMARY:Caldav child task
|
||||||
|
CREATED:20230301T073337Z
|
||||||
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
RELATED-TO;RELTYPE=PARENT:uid_parent_import
|
||||||
|
END:VTODO`
|
||||||
|
|
||||||
|
const vtodoGrandChildTaskStub = `
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:uid_grand_child_import
|
||||||
|
DTSTAMP:20230301T073337Z
|
||||||
|
SUMMARY:Caldav grand child task
|
||||||
|
CREATED:20230301T073337Z
|
||||||
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
RELATED-TO;RELTYPE=PARENT:uid_child_import
|
||||||
|
END:VTODO`
|
||||||
|
|
||||||
|
e, _ := setupTestEnv()
|
||||||
|
|
||||||
|
const parentVTODO = vtodoHeader + vtodoParentTaskStub + vtodoFooter
|
||||||
|
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, parentVTODO, nil, map[string]string{"project": "36", "task": "uid_parent_import"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||||
|
|
||||||
|
const childVTODO = vtodoHeader + vtodoChildTaskStub + vtodoFooter
|
||||||
|
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, childVTODO, nil, map[string]string{"project": "36", "task": "uid_child_import"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||||
|
|
||||||
|
const grandChildVTODO = vtodoHeader + vtodoGrandChildTaskStub + vtodoFooter
|
||||||
|
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, grandChildVTODO, nil, map[string]string{"project": "36", "task": "uid_grand_child_import"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||||
|
|
||||||
|
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 200, rec.Result().StatusCode)
|
||||||
|
|
||||||
|
assert.Contains(t, rec.Body.String(), "UID:uid_parent_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_child_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "UID:uid_child_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_grand_child_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "UID:uid_grand_child_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_child_import")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Import Task & Subtask (Reverse - Subtask first)", func(t *testing.T) {
|
||||||
|
e, _ := setupTestEnv()
|
||||||
|
|
||||||
|
const vtodoGrandChildTaskStub = `
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:uid_grand_child_import
|
||||||
|
DTSTAMP:20230301T073337Z
|
||||||
|
SUMMARY:Caldav grand child task
|
||||||
|
CREATED:20230301T073337Z
|
||||||
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
RELATED-TO;RELTYPE=PARENT:uid_child_import
|
||||||
|
END:VTODO`
|
||||||
|
|
||||||
|
const grandChildVTODO = vtodoHeader + vtodoGrandChildTaskStub + vtodoFooter
|
||||||
|
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, grandChildVTODO, nil, map[string]string{"project": "36", "task": "uid_grand_child_import"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||||
|
|
||||||
|
const vtodoChildTaskStub = `BEGIN:VTODO
|
||||||
|
UID:uid_child_import
|
||||||
|
DTSTAMP:20230301T073337Z
|
||||||
|
SUMMARY:Caldav child task
|
||||||
|
CREATED:20230301T073337Z
|
||||||
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
RELATED-TO;RELTYPE=PARENT:uid_parent_import
|
||||||
|
RELATED-TO;RELTYPE=CHILD:uid_grand_child_import
|
||||||
|
END:VTODO`
|
||||||
|
|
||||||
|
const childVTODO = vtodoHeader + vtodoChildTaskStub + vtodoFooter
|
||||||
|
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, childVTODO, nil, map[string]string{"project": "36", "task": "uid_child_import"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||||
|
|
||||||
|
const vtodoParentTaskStub = `BEGIN:VTODO
|
||||||
|
UID:uid_parent_import
|
||||||
|
DTSTAMP:20230301T073337Z
|
||||||
|
SUMMARY:Caldav parent task
|
||||||
|
CREATED:20230301T073337Z
|
||||||
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
RELATED-TO;RELTYPE=CHILD:uid_child_import
|
||||||
|
END:VTODO`
|
||||||
|
|
||||||
|
const parentVTODO = vtodoHeader + vtodoParentTaskStub + vtodoFooter
|
||||||
|
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, parentVTODO, nil, map[string]string{"project": "36", "task": "uid_parent_import"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||||
|
|
||||||
|
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 200, rec.Result().StatusCode)
|
||||||
|
|
||||||
|
assert.Contains(t, rec.Body.String(), "UID:uid_parent_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_child_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "UID:uid_child_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_grand_child_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "UID:uid_grand_child_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_child_import")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Delete Subtask", func(t *testing.T) {
|
||||||
|
e, _ := setupTestEnv()
|
||||||
|
|
||||||
|
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodDelete, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-child-task"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 204, rec.Result().StatusCode)
|
||||||
|
|
||||||
|
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodDelete, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-child-task-2"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 204, rec.Result().StatusCode)
|
||||||
|
|
||||||
|
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-parent-task"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 200, rec.Result().StatusCode)
|
||||||
|
|
||||||
|
assert.NotContains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid-caldav-test-child-task")
|
||||||
|
assert.NotContains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid-caldav-test-child-task-2")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Delete Parent Task", func(t *testing.T) {
|
||||||
|
e, _ := setupTestEnv()
|
||||||
|
|
||||||
|
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodDelete, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-parent-task"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 204, rec.Result().StatusCode)
|
||||||
|
|
||||||
|
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-child-task"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 200, rec.Result().StatusCode)
|
||||||
|
|
||||||
|
assert.NotContains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task")
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaldavSubtasksDifferentLists(t *testing.T) {
|
||||||
|
t.Run("Import Parent Task & Child Task Different Lists", func(t *testing.T) {
|
||||||
|
const vtodoParentTask = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
X-PUBLISHED-TTL:PT4H
|
||||||
|
X-WR-CALNAME:Project 36 for Caldav tests
|
||||||
|
PRODID:-//Vikunja Todo App//EN
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:uid_parent_import
|
||||||
|
DTSTAMP:20230301T073337Z
|
||||||
|
SUMMARY:Caldav parent task
|
||||||
|
CREATED:20230301T073337Z
|
||||||
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR`
|
||||||
|
|
||||||
|
const vtodoChildTask = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
X-PUBLISHED-TTL:PT4H
|
||||||
|
X-WR-CALNAME:Project 38 for Caldav tests
|
||||||
|
PRODID:-//Vikunja Todo App//EN
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:uid_child_import
|
||||||
|
DTSTAMP:20230301T073337Z
|
||||||
|
SUMMARY:Caldav child task
|
||||||
|
CREATED:20230301T073337Z
|
||||||
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
RELATED-TO;RELTYPE=PARENT:uid_parent_import
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR`
|
||||||
|
|
||||||
|
e, _ := setupTestEnv()
|
||||||
|
|
||||||
|
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoParentTask, nil, map[string]string{"project": "36", "task": "uid_parent_import"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, rec.Result().StatusCode, 201)
|
||||||
|
|
||||||
|
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoChildTask, nil, map[string]string{"project": "38", "task": "uid_child_import"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, rec.Result().StatusCode, 201)
|
||||||
|
|
||||||
|
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid_parent_import"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, rec.Result().StatusCode, 200)
|
||||||
|
assert.Contains(t, rec.Body.String(), "UID:uid_parent_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_child_import")
|
||||||
|
|
||||||
|
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "38", "task": "uid_child_import"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, rec.Result().StatusCode, 200)
|
||||||
|
assert.Contains(t, rec.Body.String(), "UID:uid_child_import")
|
||||||
|
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_import")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Check relationships across lists", func(t *testing.T) {
|
||||||
|
e, _ := setupTestEnv()
|
||||||
|
|
||||||
|
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-parent-task-another-list"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, rec.Result().StatusCode, 200)
|
||||||
|
assert.Contains(t, rec.Body.String(), "UID:uid-caldav-test-parent-task-another-list")
|
||||||
|
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid-caldav-test-child-task-another-list")
|
||||||
|
|
||||||
|
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "38", "task": "uid-caldav-test-child-task-another-list"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, rec.Result().StatusCode, 200)
|
||||||
|
assert.Contains(t, rec.Body.String(), "UID:uid-caldav-test-child-task-another-list")
|
||||||
|
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task-another-list")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -79,23 +79,36 @@ 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 createRequest(e *echo.Echo, method string, payload string, queryParam url.Values, urlParams map[string]string) (c echo.Context, rec *httptest.ResponseRecorder) {
|
||||||
// Setup
|
|
||||||
e, err := setupTestEnv()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Do the actual request
|
|
||||||
req := httptest.NewRequest(method, "/", strings.NewReader(payload))
|
req := httptest.NewRequest(method, "/", strings.NewReader(payload))
|
||||||
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
||||||
req.URL.RawQuery = queryParam.Encode()
|
req.URL.RawQuery = queryParam.Encode()
|
||||||
rec = httptest.NewRecorder()
|
rec = httptest.NewRecorder()
|
||||||
|
|
||||||
c = e.NewContext(req, rec)
|
c = e.NewContext(req, rec)
|
||||||
|
var paramNames []string
|
||||||
|
var paramValues []string
|
||||||
|
for name, value := range urlParams {
|
||||||
|
paramNames = append(paramNames, name)
|
||||||
|
paramValues = append(paramValues, value)
|
||||||
|
}
|
||||||
|
c.SetParamNames(paramNames...)
|
||||||
|
c.SetParamValues(paramValues...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func bootstrapTestRequest(t *testing.T, method string, payload string, queryParam url.Values, urlParams map[string]string) (c echo.Context, rec *httptest.ResponseRecorder) {
|
||||||
|
// Setup
|
||||||
|
e, err := setupTestEnv()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
c, rec = createRequest(e, method, payload, queryParam, urlParams)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
var c echo.Context
|
||||||
|
c, rec = bootstrapTestRequest(t, method, payload, queryParams, urlParams)
|
||||||
err = handler(c)
|
err = handler(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -124,36 +137,25 @@ 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) {
|
|
||||||
c, rec = bootstrapTestRequest(t, method, payload, queryParams)
|
|
||||||
|
|
||||||
var paramNames []string
|
|
||||||
var paramValues []string
|
|
||||||
for name, value := range urlParams {
|
|
||||||
paramNames = append(paramNames, name)
|
|
||||||
paramValues = append(paramValues, value)
|
|
||||||
}
|
|
||||||
c.SetParamNames(paramNames...)
|
|
||||||
c.SetParamValues(paramValues...)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
var c echo.Context
|
||||||
|
c, rec = bootstrapTestRequest(t, method, payload, queryParams, urlParams)
|
||||||
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)
|
var c echo.Context
|
||||||
|
c, rec = bootstrapTestRequest(t, method, payload, queryParams, urlParams)
|
||||||
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, e *echo.Echo, 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)
|
var c echo.Context
|
||||||
|
c, rec = createRequest(e, method, payload, queryParams, urlParams)
|
||||||
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)
|
||||||
|
@ -147,7 +147,7 @@ func TestBucket_Delete(t *testing.T) {
|
|||||||
tasks := []*Task{}
|
tasks := []*Task{}
|
||||||
err = s.Where("bucket_id = ?", 1).Find(&tasks)
|
err = s.Where("bucket_id = ?", 1).Find(&tasks)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, tasks, 16)
|
assert.Len(t, tasks, 15)
|
||||||
db.AssertMissing(t, "buckets", map[string]interface{}{
|
db.AssertMissing(t, "buckets", map[string]interface{}{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"project_id": 1,
|
"project_id": 1,
|
||||||
|
@ -373,7 +373,14 @@ func (bt *BulkTask) GetTasksByIDs(s *xorm.Session) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetTaskSimpleByUUID(s *xorm.Session, uid string) (task *Task, err error) {
|
func GetTaskSimpleByUUID(s *xorm.Session, uid string) (task *Task, err error) {
|
||||||
_, err = s.In("uid", uid).Get(task)
|
var has bool
|
||||||
|
task = &Task{}
|
||||||
|
|
||||||
|
has, err = s.In("uid", uid).Get(task)
|
||||||
|
if !has || err != nil {
|
||||||
|
return &Task{}, ErrTaskDoesNotExist{}
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package caldav
|
package caldav
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -292,6 +293,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
|
||||||
}
|
}
|
||||||
@ -316,6 +324,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
|
||||||
|
|
||||||
|
// Explicitly 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()
|
||||||
|
|
||||||
@ -343,6 +355,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
|
||||||
}
|
}
|
||||||
@ -430,6 +448,91 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removeStaleRelations(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}
|
||||||
|
// FIXME: Optimize to get only required attributes (ie. RelatedTasks).
|
||||||
|
err = existingTask.ReadOne(s, a)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for relationKind, relatedTasks := range existingTask.RelatedTasks {
|
||||||
|
|
||||||
|
for _, relatedTask := range relatedTasks {
|
||||||
|
relationInNewList := slices.ContainsFunc(newRelations[relationKind], func(newRelation *models.Task) bool { return newRelation.UID == relatedTask.UID })
|
||||||
|
|
||||||
|
if !relationInNewList {
|
||||||
|
rel := models.TaskRelation{
|
||||||
|
TaskID: task.ID,
|
||||||
|
OtherTaskID: relatedTask.ID,
|
||||||
|
RelationKind: relationKind,
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
|
||||||
|
err = removeStaleRelations(s, a, task, newRelations)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the current relations exist:
|
||||||
|
for relationType, relatedTasksInVTODO := range newRelations {
|
||||||
|
// Persist each relation independently:
|
||||||
|
for _, relatedTaskInVTODO := range relatedTasksInVTODO {
|
||||||
|
|
||||||
|
var relatedTask *models.Task
|
||||||
|
createDummy := false
|
||||||
|
|
||||||
|
// Get the task from the DB:
|
||||||
|
relatedTaskInDB, err := models.GetTaskSimpleByUUID(s, relatedTaskInVTODO.UID)
|
||||||
|
if err != nil {
|
||||||
|
relatedTask = relatedTaskInVTODO
|
||||||
|
createDummy = true
|
||||||
|
} else {
|
||||||
|
relatedTask = relatedTaskInDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// In the worst case, this was an error by the client and we are left with
|
||||||
|
// this dummy task to clean up.
|
||||||
|
if createDummy {
|
||||||
|
relatedTask.ProjectID = task.ProjectID
|
||||||
|
relatedTask.Title = "DUMMY-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
|
||||||
|
484
pkg/routes/caldav/listStorageProvider_test.go
Normal file
484
pkg/routes/caldav/listStorageProvider_test.go
Normal file
@ -0,0 +1,484 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public Licensee
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package caldav
|
||||||
|
|
||||||
|
// This file tests logic related to handling tasks in CALDAV format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
|
"code.vikunja.io/api/pkg/db"
|
||||||
|
"code.vikunja.io/api/pkg/files"
|
||||||
|
"code.vikunja.io/api/pkg/models"
|
||||||
|
"code.vikunja.io/api/pkg/user"
|
||||||
|
|
||||||
|
"github.com/samedi/caldav-go/data"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check logic related to creating sub-tasks
|
||||||
|
func TestSubTask_Create(t *testing.T) {
|
||||||
|
u := &user.User{
|
||||||
|
ID: 15,
|
||||||
|
Username: "user15",
|
||||||
|
Email: "user15@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
config.InitDefaultConfig()
|
||||||
|
files.InitTests()
|
||||||
|
user.InitTests()
|
||||||
|
models.SetupTests()
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create a subtask
|
||||||
|
//
|
||||||
|
t.Run("create", func(t *testing.T) {
|
||||||
|
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
const taskUID = "uid_child1"
|
||||||
|
const taskContent = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
X-PUBLISHED-TTL:PT4H
|
||||||
|
X-WR-CALNAME:Project 36 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-caldav-test-parent-task
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR`
|
||||||
|
|
||||||
|
storage := &VikunjaCaldavProjectStorage{
|
||||||
|
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
|
||||||
|
task: &models.Task{UID: taskUID},
|
||||||
|
user: u,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the subtask:
|
||||||
|
taskResource, err := storage.CreateResource(taskUID, taskContent)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the result CALDAV contains the relation:
|
||||||
|
content, _ := taskResource.GetContentData()
|
||||||
|
assert.Contains(t, content, "UID:"+taskUID)
|
||||||
|
assert.Contains(t, content, "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task")
|
||||||
|
|
||||||
|
// Get the task from the DB:
|
||||||
|
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task := tasks[0]
|
||||||
|
|
||||||
|
// Check that the parent-child relationship is present:
|
||||||
|
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 1)
|
||||||
|
parentTask := task.RelatedTasks[models.RelationKindParenttask][0]
|
||||||
|
assert.Equal(t, "uid-caldav-test-parent-task", parentTask.UID)
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create a subtask on a subtask, i.e. create a grand-child
|
||||||
|
//
|
||||||
|
t.Run("create grandchild on child task", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
const taskUIDChild = "uid_child1"
|
||||||
|
const taskContentChild = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
X-PUBLISHED-TTL:PT4H
|
||||||
|
X-WR-CALNAME:Project 36 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-caldav-test-parent-task
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR`
|
||||||
|
|
||||||
|
storage := &VikunjaCaldavProjectStorage{
|
||||||
|
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
|
||||||
|
task: &models.Task{UID: taskUIDChild},
|
||||||
|
user: u,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the subtask:
|
||||||
|
_, err := storage.CreateResource(taskUIDChild, taskContentChild)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
const taskUID = "uid_grand_child1"
|
||||||
|
const taskContent = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
X-PUBLISHED-TTL:PT4H
|
||||||
|
X-WR-CALNAME:Project 36 for Caldav tests
|
||||||
|
PRODID:-//Vikunja Todo App//EN
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:uid_grand_child1
|
||||||
|
DTSTAMP:20230301T073337Z
|
||||||
|
SUMMARY:Caldav grand child task 1
|
||||||
|
CREATED:20230301T073337Z
|
||||||
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
RELATED-TO;RELTYPE=PARENT:uid_child1
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR`
|
||||||
|
|
||||||
|
storage = &VikunjaCaldavProjectStorage{
|
||||||
|
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
|
||||||
|
task: &models.Task{UID: taskUID},
|
||||||
|
user: u,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the task:
|
||||||
|
var taskResource *data.Resource
|
||||||
|
taskResource, err = storage.CreateResource(taskUID, taskContent)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the result CALDAV contains the relation:
|
||||||
|
content, _ := taskResource.GetContentData()
|
||||||
|
assert.Contains(t, content, "UID:"+taskUID)
|
||||||
|
assert.Contains(t, content, "RELATED-TO;RELTYPE=PARENT:uid_child1")
|
||||||
|
|
||||||
|
// Get the task from the DB:
|
||||||
|
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task := tasks[0]
|
||||||
|
|
||||||
|
// Check that the parent-child relationship of the grandchildren is present:
|
||||||
|
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 1)
|
||||||
|
parentTask := task.RelatedTasks[models.RelationKindParenttask][0]
|
||||||
|
assert.Equal(t, "uid_child1", parentTask.UID)
|
||||||
|
|
||||||
|
// Get the child task and check that it now has a parent and a child:
|
||||||
|
tasks, err = models.GetTasksByUIDs(s, []string{"uid_child1"}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task = tasks[0]
|
||||||
|
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 1)
|
||||||
|
parentTask = task.RelatedTasks[models.RelationKindParenttask][0]
|
||||||
|
assert.Equal(t, "uid-caldav-test-parent-task", parentTask.UID)
|
||||||
|
assert.Len(t, task.RelatedTasks[models.RelationKindSubtask], 1)
|
||||||
|
childTask := task.RelatedTasks[models.RelationKindSubtask][0]
|
||||||
|
assert.Equal(t, taskUID, childTask.UID)
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create a subtask on a parent that we don't know anything about (yet)
|
||||||
|
//
|
||||||
|
t.Run("create subtask on unknown parent", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Create a subtask:
|
||||||
|
const taskUID = "uid_child1"
|
||||||
|
const taskContent = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
X-PUBLISHED-TTL:PT4H
|
||||||
|
X-WR-CALNAME:Project 36 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-caldav-test-parent-doesnt-exist-yet
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR`
|
||||||
|
|
||||||
|
storage := &VikunjaCaldavProjectStorage{
|
||||||
|
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
|
||||||
|
task: &models.Task{UID: taskUID},
|
||||||
|
user: u,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the task:
|
||||||
|
taskResource, err := storage.CreateResource(taskUID, taskContent)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the result CALDAV contains the relation:
|
||||||
|
content, _ := taskResource.GetContentData()
|
||||||
|
assert.Contains(t, content, "UID:"+taskUID)
|
||||||
|
assert.Contains(t, content, "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-doesnt-exist-yet")
|
||||||
|
|
||||||
|
// Get the task from the DB:
|
||||||
|
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task := tasks[0]
|
||||||
|
|
||||||
|
// Check that the parent-child relationship is present:
|
||||||
|
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 1)
|
||||||
|
parentTask := task.RelatedTasks[models.RelationKindParenttask][0]
|
||||||
|
assert.Equal(t, "uid-caldav-test-parent-doesnt-exist-yet", parentTask.UID)
|
||||||
|
|
||||||
|
// Check that the non-existent parent task was created in the process:
|
||||||
|
tasks, err = models.GetTasksByUIDs(s, []string{"uid-caldav-test-parent-doesnt-exist-yet"}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task = tasks[0]
|
||||||
|
assert.Equal(t, "uid-caldav-test-parent-doesnt-exist-yet", task.UID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic related to editing tasks and subtasks
|
||||||
|
func TestSubTask_Update(t *testing.T) {
|
||||||
|
u := &user.User{
|
||||||
|
ID: 15,
|
||||||
|
Username: "user15",
|
||||||
|
Email: "user15@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Edit a subtask and check that the relations are not gone
|
||||||
|
//
|
||||||
|
t.Run("edit subtask", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Edit the subtask:
|
||||||
|
const taskUID = "uid-caldav-test-child-task"
|
||||||
|
const taskContent = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
X-PUBLISHED-TTL:PT4H
|
||||||
|
X-WR-CALNAME:Project 36 for Caldav tests
|
||||||
|
PRODID:-//Vikunja Todo App//EN
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:uid-caldav-test-child-task
|
||||||
|
DTSTAMP:20230301T073337Z
|
||||||
|
SUMMARY:Child task for Caldav Test (edited)
|
||||||
|
CREATED:20230301T073337Z
|
||||||
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR`
|
||||||
|
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task := tasks[0]
|
||||||
|
storage := &VikunjaCaldavProjectStorage{
|
||||||
|
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
|
||||||
|
task: task,
|
||||||
|
user: u,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit the task:
|
||||||
|
taskResource, err := storage.UpdateResource(taskUID, taskContent)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the result CALDAV still contains the relation:
|
||||||
|
content, _ := taskResource.GetContentData()
|
||||||
|
assert.Contains(t, content, "UID:"+taskUID)
|
||||||
|
assert.Contains(t, content, "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task")
|
||||||
|
|
||||||
|
// Get the task from the DB:
|
||||||
|
tasks, err = models.GetTasksByUIDs(s, []string{taskUID}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task = tasks[0]
|
||||||
|
|
||||||
|
// Check that the parent-child relationship is still present:
|
||||||
|
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 1)
|
||||||
|
parentTask := task.RelatedTasks[models.RelationKindParenttask][0]
|
||||||
|
assert.Equal(t, "uid-caldav-test-parent-task", parentTask.UID)
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// Edit a parent task and check that the subtasks are still linked
|
||||||
|
//
|
||||||
|
t.Run("edit parent", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Edit the parent task:
|
||||||
|
const taskUID = "uid-caldav-test-parent-task"
|
||||||
|
const taskContent = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
X-PUBLISHED-TTL:PT4H
|
||||||
|
X-WR-CALNAME:Project 36 for Caldav tests
|
||||||
|
PRODID:-//Vikunja Todo App//EN
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:uid-caldav-test-parent-task
|
||||||
|
DTSTAMP:20230301T073337Z
|
||||||
|
SUMMARY:Parent task for Caldav Test (edited)
|
||||||
|
CREATED:20230301T073337Z
|
||||||
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
RELATED-TO;RELTYPE=CHILD:uid-caldav-test-child-task
|
||||||
|
RELATED-TO;RELTYPE=CHILD:uid-caldav-test-child-task-2
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR`
|
||||||
|
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task := tasks[0]
|
||||||
|
storage := &VikunjaCaldavProjectStorage{
|
||||||
|
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
|
||||||
|
task: task,
|
||||||
|
user: u,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit the task:
|
||||||
|
_, err = storage.UpdateResource(taskUID, taskContent)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Get the task from the DB:
|
||||||
|
tasks, err = models.GetTasksByUIDs(s, []string{taskUID}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task = tasks[0]
|
||||||
|
|
||||||
|
// Check that the subtasks are still linked:
|
||||||
|
assert.Len(t, task.RelatedTasks[models.RelationKindSubtask], 2)
|
||||||
|
existingSubTask := task.RelatedTasks[models.RelationKindSubtask][0]
|
||||||
|
assert.Equal(t, "uid-caldav-test-child-task", existingSubTask.UID)
|
||||||
|
existingSubTask = task.RelatedTasks[models.RelationKindSubtask][1]
|
||||||
|
assert.Equal(t, "uid-caldav-test-child-task-2", existingSubTask.UID)
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// Edit a subtask and change its parent
|
||||||
|
//
|
||||||
|
t.Run("edit subtask change parent", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Edit the subtask:
|
||||||
|
const taskUID = "uid-caldav-test-child-task"
|
||||||
|
const taskContent = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
X-PUBLISHED-TTL:PT4H
|
||||||
|
X-WR-CALNAME:Project 36 for Caldav tests
|
||||||
|
PRODID:-//Vikunja Todo App//EN
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:uid-caldav-test-child-task
|
||||||
|
DTSTAMP:20230301T073337Z
|
||||||
|
SUMMARY:Child task for Caldav Test (edited)
|
||||||
|
CREATED:20230301T073337Z
|
||||||
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task-2
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR`
|
||||||
|
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task := tasks[0]
|
||||||
|
storage := &VikunjaCaldavProjectStorage{
|
||||||
|
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
|
||||||
|
task: task,
|
||||||
|
user: u,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit the task:
|
||||||
|
taskResource, err := storage.UpdateResource(taskUID, taskContent)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the result CALDAV contains the new relation:
|
||||||
|
content, _ := taskResource.GetContentData()
|
||||||
|
assert.Contains(t, content, "UID:"+taskUID)
|
||||||
|
assert.Contains(t, content, "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task-2")
|
||||||
|
|
||||||
|
// Get the task from the DB:
|
||||||
|
tasks, err = models.GetTasksByUIDs(s, []string{taskUID}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task = tasks[0]
|
||||||
|
|
||||||
|
// Check that the parent-child relationship has changed to the new parent:
|
||||||
|
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 1)
|
||||||
|
parentTask := task.RelatedTasks[models.RelationKindParenttask][0]
|
||||||
|
assert.Equal(t, "uid-caldav-test-parent-task-2", parentTask.UID)
|
||||||
|
|
||||||
|
// Get the previous parent from the DB and check that its previous child is gone:
|
||||||
|
tasks, err = models.GetTasksByUIDs(s, []string{"uid-caldav-test-parent-task"}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task = tasks[0]
|
||||||
|
assert.Len(t, task.RelatedTasks[models.RelationKindSubtask], 1)
|
||||||
|
// We're gone, but our former sibling is still there:
|
||||||
|
formerSiblingSubTask := task.RelatedTasks[models.RelationKindSubtask][0]
|
||||||
|
assert.Equal(t, "uid-caldav-test-child-task-2", formerSiblingSubTask.UID)
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// Edit a subtask and remove its parent
|
||||||
|
//
|
||||||
|
t.Run("edit subtask remove parent", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Edit the subtask:
|
||||||
|
const taskUID = "uid-caldav-test-child-task"
|
||||||
|
const taskContent = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:PUBLISH
|
||||||
|
X-PUBLISHED-TTL:PT4H
|
||||||
|
X-WR-CALNAME:Project 36 for Caldav tests
|
||||||
|
PRODID:-//Vikunja Todo App//EN
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:uid-caldav-test-child-task
|
||||||
|
DTSTAMP:20230301T073337Z
|
||||||
|
SUMMARY:Child task for Caldav Test (edited)
|
||||||
|
CREATED:20230301T073337Z
|
||||||
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR`
|
||||||
|
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task := tasks[0]
|
||||||
|
storage := &VikunjaCaldavProjectStorage{
|
||||||
|
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
|
||||||
|
task: task,
|
||||||
|
user: u,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit the task:
|
||||||
|
taskResource, err := storage.UpdateResource(taskUID, taskContent)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the result CALDAV contains the new relation:
|
||||||
|
content, _ := taskResource.GetContentData()
|
||||||
|
assert.Contains(t, content, "UID:"+taskUID)
|
||||||
|
assert.NotContains(t, content, "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task")
|
||||||
|
|
||||||
|
// Get the task from the DB:
|
||||||
|
tasks, err = models.GetTasksByUIDs(s, []string{taskUID}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task = tasks[0]
|
||||||
|
|
||||||
|
// Check that the parent-child relationship is gone:
|
||||||
|
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 0)
|
||||||
|
|
||||||
|
// Get the previous parent from the DB and check that its child is gone:
|
||||||
|
tasks, err = models.GetTasksByUIDs(s, []string{"uid-caldav-test-parent-task"}, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
task = tasks[0]
|
||||||
|
// We're gone, but our former sibling is still there:
|
||||||
|
assert.Len(t, task.RelatedTasks[models.RelationKindSubtask], 1)
|
||||||
|
formerSiblingSubTask := task.RelatedTasks[models.RelationKindSubtask][0]
|
||||||
|
assert.Equal(t, "uid-caldav-test-child-task-2", formerSiblingSubTask.UID)
|
||||||
|
})
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user