From 56ce73738dcb02229dfa7ab05fba43057d1f0d94 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 7 Apr 2026 13:58:04 +0200 Subject: [PATCH] test(migration): add WeKan migration tests and fixture Add comprehensive tests for the WeKan conversion function including edge cases (empty board, orphan cards, color mapping, multiple checklists, unsupported fields) and a realistic JSON fixture file. --- .../wekan/testdata_wekan_export.json | 67 ++++ pkg/modules/migration/wekan/wekan_test.go | 322 ++++++++++++++++++ 2 files changed, 389 insertions(+) create mode 100644 pkg/modules/migration/wekan/testdata_wekan_export.json create mode 100644 pkg/modules/migration/wekan/wekan_test.go diff --git a/pkg/modules/migration/wekan/testdata_wekan_export.json b/pkg/modules/migration/wekan/testdata_wekan_export.json new file mode 100644 index 000000000..008a217c0 --- /dev/null +++ b/pkg/modules/migration/wekan/testdata_wekan_export.json @@ -0,0 +1,67 @@ +{ + "_format": "wekan-board-1.0.0", + "_id": "testboard123", + "title": "Sample Project Board", + "labels": [ + {"_id": "lbl-bug", "name": "Bug", "color": "red"}, + {"_id": "lbl-feat", "name": "Feature", "color": "green"}, + {"_id": "lbl-urgent", "name": "Urgent", "color": "orange"} + ], + "lists": [ + {"_id": "list-backlog", "title": "Backlog", "sort": 0, "archived": false}, + {"_id": "list-progress", "title": "In Progress", "sort": 1, "archived": false}, + {"_id": "list-done", "title": "Done", "sort": 2, "archived": false} + ], + "cards": [ + { + "_id": "card-1", + "title": "Fix authentication flow", + "description": "Users are getting logged out unexpectedly.", + "listId": "list-backlog", + "labelIds": ["lbl-bug", "lbl-urgent"], + "sort": 0, + "archived": false, + "startAt": "2024-03-01T09:00:00.000Z", + "dueAt": "2024-03-15T17:00:00.000Z", + "createdAt": "2024-02-20T10:00:00.000Z" + }, + { + "_id": "card-2", + "title": "Add dark mode", + "description": "Implement dark mode toggle in settings.", + "listId": "list-progress", + "labelIds": ["lbl-feat"], + "sort": 0, + "archived": false, + "createdAt": "2024-02-25T08:00:00.000Z" + }, + { + "_id": "card-3", + "title": "Update README", + "description": "", + "listId": "list-done", + "labelIds": [], + "sort": 0, + "archived": true, + "createdAt": "2024-01-10T12:00:00.000Z" + } + ], + "checklists": [ + {"_id": "cl-1", "cardId": "card-1", "title": "Steps", "sort": 0} + ], + "checklistItems": [ + {"_id": "cli-1", "checklistId": "cl-1", "cardId": "card-1", "title": "Reproduce the issue", "sort": 0, "isFinished": true}, + {"_id": "cli-2", "checklistId": "cl-1", "cardId": "card-1", "title": "Write failing test", "sort": 1, "isFinished": false}, + {"_id": "cli-3", "checklistId": "cl-1", "cardId": "card-1", "title": "Fix the code", "sort": 2, "isFinished": false} + ], + "comments": [ + {"_id": "com-1", "text": "I can reproduce this on Firefox too.", "createdAt": "2024-03-02T11:30:00.000Z", "cardId": "card-1"}, + {"_id": "com-2", "text": "Should we support system theme detection?", "createdAt": "2024-02-26T09:00:00.000Z", "cardId": "card-2"} + ], + "swimlanes": [], + "activities": [], + "rules": [], + "triggers": [], + "actions": [], + "customFields": [] +} diff --git a/pkg/modules/migration/wekan/wekan_test.go b/pkg/modules/migration/wekan/wekan_test.go new file mode 100644 index 000000000..942af8dda --- /dev/null +++ b/pkg/modules/migration/wekan/wekan_test.go @@ -0,0 +1,322 @@ +// 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 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) + + // Card 3 - archived + task3 := project.Tasks[2] + assert.Equal(t, "Update README", task3.Title) + assert.True(t, task3.Done) +}