Files
pad/internal/items/migrate_test.go
T
xarmian 6daa8eb68b Add move item between collections with field migration
Full-stack feature: move items between collections (e.g., idea → task)
with automatic field migration.

Backend:
- Field migration engine (items/migrate.go) maps matching fields,
  handles type conversions, drops incompatible fields, applies defaults
- Store method updates collection_id and assigns new item_number
- POST /api/v1/workspaces/{ws}/items/{slug}/move endpoint
- Activity logging with "moved" action and from/to metadata
- 6 migration unit tests covering type matching, conversion, and edge cases

CLI:
- pad move <slug> <target-collection> [--field key=value ...]
- Accepts singular collection names (task, idea, bug, etc.)

Web UI:
- "Move to..." dropdown on item detail page
- Shows all collections except current with icons
- Redirects to the item's new URL after move

Field migration rules:
- Same type: transfer directly (validate select options)
- Compatible types (text↔url, number→text, select→text): auto-convert
- Incompatible types: drop silently
- Missing required target fields: apply defaults or error
2026-03-28 05:01:40 +00:00

118 lines
3.5 KiB
Go

package items
import (
"testing"
"github.com/xarmian/pad/internal/models"
)
func TestMigrateFields_MatchingTypes(t *testing.T) {
source := []models.FieldDef{
{Key: "status", Type: "select", Options: []string{"open", "done"}},
{Key: "priority", Type: "select", Options: []string{"low", "high"}},
}
target := []models.FieldDef{
{Key: "status", Type: "select", Options: []string{"open", "closed"}, Required: true},
{Key: "priority", Type: "select", Options: []string{"low", "medium", "high"}},
}
fields := map[string]any{"status": "open", "priority": "high"}
result := MigrateFields(fields, source, target)
if result.Fields["status"] != "open" {
t.Errorf("status: got %v, want 'open'", result.Fields["status"])
}
if result.Fields["priority"] != "high" {
t.Errorf("priority: got %v, want 'high'", result.Fields["priority"])
}
if len(result.Dropped) != 0 {
t.Errorf("dropped: got %v, want none", result.Dropped)
}
}
func TestMigrateFields_SelectValueNotInTarget(t *testing.T) {
source := []models.FieldDef{
{Key: "status", Type: "select", Options: []string{"open", "in-progress", "done"}},
}
target := []models.FieldDef{
{Key: "status", Type: "select", Options: []string{"todo", "doing", "done"}, Required: true, Default: "todo"},
}
fields := map[string]any{"status": "in-progress"}
result := MigrateFields(fields, source, target)
// "in-progress" is not in target options, should be dropped and default applied
if result.Fields["status"] != "todo" {
t.Errorf("status: got %v, want 'todo' (default after drop)", result.Fields["status"])
}
}
func TestMigrateFields_DropsExtraFields(t *testing.T) {
source := []models.FieldDef{
{Key: "severity", Type: "select", Options: []string{"low", "high"}},
{Key: "browser", Type: "text"},
}
target := []models.FieldDef{
{Key: "priority", Type: "select", Options: []string{"low", "high"}},
}
fields := map[string]any{"severity": "high", "browser": "Chrome"}
result := MigrateFields(fields, source, target)
if len(result.Dropped) != 2 {
t.Errorf("dropped: got %d, want 2", len(result.Dropped))
}
}
func TestMigrateFields_TypeConversion(t *testing.T) {
source := []models.FieldDef{
{Key: "count", Type: "number"},
{Key: "status", Type: "select", Options: []string{"open"}},
}
target := []models.FieldDef{
{Key: "count", Type: "text"},
{Key: "status", Type: "text"},
}
fields := map[string]any{"count": 42, "status": "open"}
result := MigrateFields(fields, source, target)
if result.Fields["count"] != "42" {
t.Errorf("count: got %v, want '42'", result.Fields["count"])
}
if result.Fields["status"] != "open" {
t.Errorf("status: got %v, want 'open'", result.Fields["status"])
}
}
func TestMigrateFields_RequiredFieldMissing(t *testing.T) {
source := []models.FieldDef{}
target := []models.FieldDef{
{Key: "status", Type: "select", Options: []string{"open"}, Required: true},
}
fields := map[string]any{}
result := MigrateFields(fields, source, target)
if len(result.Errors) != 1 {
t.Errorf("errors: got %d, want 1", len(result.Errors))
}
}
func TestMigrateFields_DefaultApplied(t *testing.T) {
source := []models.FieldDef{}
target := []models.FieldDef{
{Key: "status", Type: "select", Options: []string{"open", "done"}, Required: true, Default: "open"},
}
fields := map[string]any{}
result := MigrateFields(fields, source, target)
if result.Fields["status"] != "open" {
t.Errorf("status: got %v, want 'open'", result.Fields["status"])
}
if len(result.Errors) != 0 {
t.Errorf("errors: got %v, want none", result.Errors)
}
}