// 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 License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package wekan import ( "bytes" "os" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestConvertWekanToVikunja(t *testing.T) { startDate := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC) dueDate := time.Date(2024, 2, 15, 10, 0, 0, 0, time.UTC) createdAt := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) commentDate := time.Date(2024, 1, 20, 14, 30, 0, 0, time.UTC) board := &wekanBoard{ ID: "board1", Title: "My Board", Labels: []wekanLabel{ {ID: "lbl1", Name: "Bug", Color: "red"}, {ID: "lbl2", Name: "Feature", Color: "green"}, {ID: "lbl3", Name: "", Color: "blue"}, }, Lists: []wekanList{ {ID: "list1", Title: "To Do", Sort: 1}, {ID: "list2", Title: "In Progress", Sort: 2}, }, Cards: []wekanCard{ { ID: "card1", Title: "Fix login bug", Description: "The login page crashes", ListID: "list1", LabelIDs: []string{"lbl1"}, Sort: 1, StartAt: &startDate, DueAt: &dueDate, CreatedAt: &createdAt, }, { ID: "card2", Title: "Add dashboard", ListID: "list2", LabelIDs: []string{"lbl2", "lbl3"}, Sort: 1, }, { ID: "card3", Title: "Archived task", ListID: "list1", Sort: 2, Archived: true, }, }, Checklists: []wekanChecklist{ {ID: "cl1", CardID: "card1", Title: "Steps to reproduce", Sort: 1}, }, ChecklistItems: []wekanChecklistItem{ {ID: "cli1", ChecklistID: "cl1", CardID: "card1", Title: "Open browser", Sort: 1, IsFinished: true}, {ID: "cli2", ChecklistID: "cl1", CardID: "card1", Title: "Click login", Sort: 2, IsFinished: false}, }, Comments: []wekanComment{ {ID: "com1", Text: "This is urgent", CreatedAt: &commentDate, CardID: "card1"}, }, } result := convertWekanToVikunja(board) // Should have 1 project (the board itself) require.Len(t, result, 1) project := result[0] assert.Equal(t, "My Board", project.Title) // Should have 2 buckets (one per list) require.Len(t, project.Buckets, 2) assert.Equal(t, "To Do", project.Buckets[0].Title) assert.Equal(t, "In Progress", project.Buckets[1].Title) // Should have 3 tasks require.Len(t, project.Tasks, 3) // Task 1: Fix login bug task1 := project.Tasks[0] assert.Equal(t, "Fix login bug", task1.Title) assert.Contains(t, task1.Description, "The login page crashes") assert.Equal(t, startDate, task1.StartDate) assert.Equal(t, dueDate, task1.DueDate) // Should be in bucket 1 (To Do) assert.Equal(t, int64(1), task1.BucketID) // Labels on task 1 require.Len(t, task1.Labels, 1) assert.Equal(t, "Bug", task1.Labels[0].Title) assert.Equal(t, "eb4646", task1.Labels[0].HexColor) // red mapped // Checklist should be appended to description as HTML task list assert.Contains(t, task1.Description, "Steps to reproduce") assert.Contains(t, task1.Description, "Open browser") assert.Contains(t, task1.Description, "Click login") assert.Contains(t, task1.Description, `checked="checked"`) // first item is done // Comment on task 1 (markdown converted to HTML) require.Len(t, task1.Comments, 1) assert.Contains(t, task1.Comments[0].Comment, "This is urgent") assert.Equal(t, commentDate, task1.Comments[0].Created) // Task 2: Add dashboard task2 := project.Tasks[1] assert.Equal(t, "Add dashboard", task2.Title) assert.Equal(t, int64(2), task2.BucketID) // In Progress require.Len(t, task2.Labels, 2) assert.Equal(t, "Feature", task2.Labels[0].Title) assert.Equal(t, "3cb500", task2.Labels[0].HexColor) // green mapped // Label with empty name but color "blue" should still be created with the color name as title assert.Equal(t, "blue", task2.Labels[1].Title) // Task 3: Archived → done task3 := project.Tasks[2] assert.Equal(t, "Archived task", task3.Title) assert.True(t, task3.Done) assert.Equal(t, int64(1), task3.BucketID) // To Do } func TestParseWekanJSON_ParsesAttachments(t *testing.T) { raw := []byte(`{ "_id": "b1", "title": "B", "lists": [], "cards": [], "attachments": [ {"_id": "a1", "cardId": "c1", "file": "aGVsbG8=", "name": "hi.txt", "type": "text/plain"} ] }`) board, err := parseWekanJSON(bytes.NewReader(raw)) require.NoError(t, err) require.Len(t, board.Attachments, 1) assert.Equal(t, "a1", board.Attachments[0].ID) assert.Equal(t, "c1", board.Attachments[0].CardID) assert.Equal(t, "aGVsbG8=", board.Attachments[0].File) assert.Equal(t, "hi.txt", board.Attachments[0].Name) assert.Equal(t, "text/plain", board.Attachments[0].Type) } func TestConvertWekanToVikunja_Attachments(t *testing.T) { // "hello" in base64 is "aGVsbG8=" board := &wekanBoard{ ID: "b1", Title: "B", Lists: []wekanList{{ID: "l1", Title: "L", Sort: 0}}, Cards: []wekanCard{{ID: "c1", Title: "Card", ListID: "l1"}}, Attachments: []wekanAttachment{ {ID: "a1", CardID: "c1", File: "aGVsbG8=", Name: "hi.txt", Type: "text/plain"}, {ID: "a2", CardID: "c1", File: "d29ybGQ=", Name: "w.txt", Type: "text/plain"}, {ID: "a3", CardID: "missing", File: "aGVsbG8=", Name: "orphan.txt", Type: "text/plain"}, }, } projects := convertWekanToVikunja(board) require.Len(t, projects, 1) require.Len(t, projects[0].Tasks, 1) task := projects[0].Tasks[0] require.Len(t, task.Attachments, 2) assert.Equal(t, "hi.txt", task.Attachments[0].File.Name) assert.Equal(t, "text/plain", task.Attachments[0].File.Mime) assert.Equal(t, []byte("hello"), task.Attachments[0].File.FileContent) assert.Equal(t, uint64(5), task.Attachments[0].File.Size) assert.Equal(t, "w.txt", task.Attachments[1].File.Name) assert.Equal(t, []byte("world"), task.Attachments[1].File.FileContent) } func TestMigrateValidJSON(t *testing.T) { validJSON := `{ "_id": "board1", "title": "Test Board", "labels": [{"_id": "l1", "name": "Bug", "color": "red"}], "lists": [{"_id": "list1", "title": "To Do", "sort": 1}], "cards": [{"_id": "c1", "title": "Task 1", "listId": "list1", "sort": 1, "labelIds": ["l1"]}], "checklists": [], "checklistItems": [], "comments": [] }` m := &Migrator{} assert.Equal(t, "wekan", m.Name()) // Test that parsing works without error r := bytes.NewReader([]byte(validJSON)) board, err := parseWekanJSON(r) require.NoError(t, err) assert.Equal(t, "Test Board", board.Title) require.Len(t, board.Cards, 1) assert.Equal(t, "Task 1", board.Cards[0].Title) } func TestParseWekanJSONInvalid(t *testing.T) { _, err := parseWekanJSON(bytes.NewReader([]byte("not json"))) require.Error(t, err) } func TestParseWekanJSONEmpty(t *testing.T) { _, err := parseWekanJSON(bytes.NewReader([]byte(""))) require.Error(t, err) } func TestConvertWekanEmptyBoard(t *testing.T) { board := &wekanBoard{ Title: "Empty Board", } result := convertWekanToVikunja(board) require.Len(t, result, 1) assert.Equal(t, "Empty Board", result[0].Title) assert.Empty(t, result[0].Tasks) assert.Empty(t, result[0].Buckets) } func TestConvertWekanCardWithoutList(t *testing.T) { // Card references a list that doesn't exist board := &wekanBoard{ Title: "Board", Lists: []wekanList{{ID: "list1", Title: "To Do", Sort: 1}}, Cards: []wekanCard{ {ID: "c1", Title: "Orphan card", ListID: "nonexistent", Sort: 1}, {ID: "c2", Title: "Normal card", ListID: "list1", Sort: 2}, }, } result := convertWekanToVikunja(board) require.Len(t, result, 1) // Orphan card should still be created, just with bucket ID 0 require.Len(t, result[0].Tasks, 2) assert.Equal(t, int64(0), result[0].Tasks[0].BucketID) assert.Equal(t, int64(1), result[0].Tasks[1].BucketID) } func TestConvertWekanLabelColorMapping(t *testing.T) { board := &wekanBoard{ Title: "Board", Labels: []wekanLabel{ {ID: "l1", Name: "Urgent", Color: "red"}, {ID: "l2", Name: "Nice", Color: "green"}, {ID: "l3", Name: "Unknown color", Color: "chartreuse"}, }, Lists: []wekanList{{ID: "list1", Title: "List", Sort: 1}}, Cards: []wekanCard{ {ID: "c1", Title: "Task", ListID: "list1", LabelIDs: []string{"l1", "l2", "l3"}, Sort: 1}, }, } result := convertWekanToVikunja(board) labels := result[0].Tasks[0].Labels require.Len(t, labels, 3) assert.Equal(t, "eb4646", labels[0].HexColor) // red assert.Equal(t, "3cb500", labels[1].HexColor) // green assert.Empty(t, labels[2].HexColor) // unknown color has no hex } func TestConvertWekanMultipleChecklists(t *testing.T) { board := &wekanBoard{ Title: "Board", Lists: []wekanList{{ID: "list1", Title: "List", Sort: 1}}, Cards: []wekanCard{{ID: "c1", Title: "Task", ListID: "list1", Sort: 1}}, Checklists: []wekanChecklist{ {ID: "cl1", CardID: "c1", Title: "Checklist A", Sort: 1}, {ID: "cl2", CardID: "c1", Title: "Checklist B", Sort: 2}, }, ChecklistItems: []wekanChecklistItem{ {ID: "i1", ChecklistID: "cl1", CardID: "c1", Title: "Item A1", Sort: 1, IsFinished: false}, {ID: "i2", ChecklistID: "cl2", CardID: "c1", Title: "Item B1", Sort: 1, IsFinished: true}, }, } result := convertWekanToVikunja(board) desc := result[0].Tasks[0].Description assert.Contains(t, desc, "Checklist A") assert.Contains(t, desc, "Checklist B") assert.Contains(t, desc, "Item A1") assert.Contains(t, desc, "Item B1") } func TestParseWekanUnsupportedFieldsIgnored(t *testing.T) { // WeKan exports include fields we don't import (swimlanes, activities, rules, etc.). // Verify they are silently ignored and parsing succeeds. jsonWithExtras := `{ "_id": "board1", "title": "Board With Extras", "labels": [], "lists": [{"_id": "list1", "title": "List", "sort": 1}], "cards": [{"_id": "c1", "title": "Task", "listId": "list1", "sort": 1}], "checklists": [], "checklistItems": [], "comments": [], "swimlanes": [{"_id": "sw1", "title": "Default"}], "activities": [{"_id": "act1", "activityType": "addComment"}], "rules": [{"_id": "rule1", "title": "Auto move"}], "triggers": [{"_id": "trig1", "activityType": "cardMove"}], "actions": [{"_id": "action1", "actionType": "moveCard"}], "customFields": [{"_id": "cf1", "name": "Priority", "type": "text"}] }` board, err := parseWekanJSON(bytes.NewReader([]byte(jsonWithExtras))) require.NoError(t, err) assert.Equal(t, "Board With Extras", board.Title) require.Len(t, board.Cards, 1) assert.Equal(t, "Task", board.Cards[0].Title) // Conversion should also work fine result := convertWekanToVikunja(board) require.Len(t, result, 1) assert.Equal(t, "Board With Extras", result[0].Title) require.Len(t, result[0].Tasks, 1) } func TestConvertWekanFromFixtureFile(t *testing.T) { file, err := os.Open("testdata_wekan_export.json") require.NoError(t, err) defer file.Close() board, err := parseWekanJSON(file) require.NoError(t, err) assert.Equal(t, "Sample Project Board", board.Title) result := convertWekanToVikunja(board) require.Len(t, result, 1) project := result[0] assert.Equal(t, "Sample Project Board", project.Title) require.Len(t, project.Buckets, 3) assert.Equal(t, "Backlog", project.Buckets[0].Title) assert.Equal(t, "In Progress", project.Buckets[1].Title) assert.Equal(t, "Done", project.Buckets[2].Title) require.Len(t, project.Tasks, 3) // Card 1 - has labels, checklist, comment, dates task1 := project.Tasks[0] assert.Equal(t, "Fix authentication flow", task1.Title) assert.Contains(t, task1.Description, "Users are getting logged out") assert.Contains(t, task1.Description, "Steps") assert.Contains(t, task1.Description, "Reproduce the issue") require.Len(t, task1.Labels, 2) require.Len(t, task1.Comments, 1) assert.False(t, task1.Done) // Attachment on card 1 require.Len(t, task1.Attachments, 1) assert.Equal(t, "note.txt", task1.Attachments[0].File.Name) assert.Equal(t, "text/plain", task1.Attachments[0].File.Mime) assert.Equal(t, []byte("hello wekan"), task1.Attachments[0].File.FileContent) // Card 3 - archived task3 := project.Tasks[2] assert.Equal(t, "Update README", task3.Title) assert.True(t, task3.Done) }