124 lines
4.8 KiB
Go
124 lines
4.8 KiB
Go
package http
|
|
|
|
import "testing"
|
|
|
|
// Kanban ordering is persisted, not just a client-side illusion: a card move
|
|
// normalizes the target column, keeps legacy status-only cards in place, and
|
|
// still honors the bookmark concurrency version (review CR-04).
|
|
func TestKanbanCardMovePersistsOrdering(t *testing.T) {
|
|
srv := testServer(t)
|
|
alice := newAPIUser(t, srv, "kanban-order@computer.test")
|
|
cols := alice.getJSON("/api/kanban/columns", 200)
|
|
if len(cols) < 2 {
|
|
t.Fatalf("expected default columns, got %d", len(cols))
|
|
}
|
|
inbox := cols[0]["id"].(string)
|
|
todo := cols[1]["id"].(string)
|
|
|
|
create := func(title, column string) map[string]any {
|
|
return alice.request("POST", "/api/bookmarks", map[string]any{
|
|
"url": "https://example.org/" + title,
|
|
"title": title,
|
|
"kanbanColumnId": column,
|
|
}, 201)
|
|
}
|
|
first := create("first", inbox)
|
|
second := create("second", inbox)
|
|
third := create("third", inbox)
|
|
|
|
// New cards start newest-first (updatedAt desc), so establish the known
|
|
// first/second/third order before asserting a reorder. Third stays at its
|
|
// original version for the move below.
|
|
for _, step := range []struct {
|
|
id string
|
|
position int
|
|
}{{first["id"].(string), 0}, {second["id"].(string), 1}} {
|
|
cur := alice.request("GET", "/api/bookmarks/"+step.id, nil, 200)
|
|
alice.request("PUT", "/api/kanban/cards/"+step.id, map[string]any{
|
|
"columnId": inbox, "position": step.position, "version": cur["version"],
|
|
}, 200)
|
|
}
|
|
|
|
// Move the newest card to the top of its own column.
|
|
moved := alice.request("PUT", "/api/kanban/cards/"+third["id"].(string), map[string]any{
|
|
"columnId": inbox, "position": 0, "version": 0,
|
|
}, 200)
|
|
if moved["kanbanPosition"] != float64(0) || moved["version"] != float64(1) {
|
|
t.Fatalf("move did not persist position/version: %v", moved)
|
|
}
|
|
if moved["kanbanColumnId"] != inbox {
|
|
t.Fatalf("move lost column: %v", moved)
|
|
}
|
|
|
|
board := func(column string) []map[string]any {
|
|
t.Helper()
|
|
out := []map[string]any{}
|
|
for _, row := range alice.getJSON("/api/bookmarks", 200) {
|
|
if row["kanbanColumnId"] == column {
|
|
out = append(out, row)
|
|
}
|
|
}
|
|
for i := 1; i < len(out); i++ {
|
|
for j := i; j > 0 && out[j]["kanbanPosition"].(float64) < out[j-1]["kanbanPosition"].(float64); j-- {
|
|
out[j], out[j-1] = out[j-1], out[j]
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
order := board(inbox)
|
|
if len(order) != 3 {
|
|
t.Fatalf("expected 3 cards, got %d", len(order))
|
|
}
|
|
for i, want := range []string{third["id"].(string), first["id"].(string), second["id"].(string)} {
|
|
if order[i]["id"] != want {
|
|
t.Fatalf("position %d: got %v, want %s", i, order[i]["id"], want)
|
|
}
|
|
if order[i]["kanbanPosition"] != float64(i) {
|
|
t.Fatalf("card %s position = %v, want %d", order[i]["id"], order[i]["kanbanPosition"], i)
|
|
}
|
|
}
|
|
|
|
// Missing/stale versions are rejected without changing order.
|
|
alice.request("PUT", "/api/kanban/cards/"+first["id"].(string), map[string]any{
|
|
"columnId": inbox, "position": 0,
|
|
}, 400)
|
|
alice.request("PUT", "/api/kanban/cards/"+third["id"].(string), map[string]any{
|
|
"columnId": inbox, "position": 0, "version": 0,
|
|
}, 409)
|
|
|
|
// Cross-column moves also normalize the target and leave the source intact.
|
|
secondNow := alice.request("GET", "/api/bookmarks/"+second["id"].(string), nil, 200)
|
|
alice.request("PUT", "/api/kanban/cards/"+second["id"].(string), map[string]any{
|
|
"columnId": todo, "position": 0, "version": secondNow["version"],
|
|
}, 200)
|
|
if got := board(todo); len(got) != 1 || got[0]["id"] != second["id"] {
|
|
t.Fatalf("cross-column move failed: %v", got)
|
|
}
|
|
if got := board(inbox); len(got) != 2 || got[0]["id"] != third["id"] || got[1]["id"] != first["id"] {
|
|
t.Fatalf("source column order changed unexpectedly: %v", got)
|
|
}
|
|
|
|
// A legacy status-only card joins the target column's explicit ordering.
|
|
legacy := alice.request("POST", "/api/bookmarks", map[string]any{
|
|
"url": "https://example.org/legacy", "title": "legacy", "kanbanStatus": "TODO",
|
|
}, 201)
|
|
alice.request("PUT", "/api/kanban/cards/"+legacy["id"].(string), map[string]any{
|
|
"columnId": todo, "position": 0, "version": 0,
|
|
}, 200)
|
|
if got := board(todo); len(got) != 2 || got[0]["id"] != legacy["id"] || got[1]["id"] != second["id"] {
|
|
t.Fatalf("legacy card did not join explicit ordering: %v", got)
|
|
}
|
|
if got := alice.request("GET", "/api/bookmarks/"+legacy["id"].(string), nil, 200); got["kanbanColumnId"] != todo {
|
|
t.Fatalf("legacy card did not gain explicit membership: %v", got)
|
|
}
|
|
|
|
// Foreign columns are never accepted.
|
|
bob := newAPIUser(t, srv, "kanban-other@computer.test")
|
|
bobCols := bob.getJSON("/api/kanban/columns", 200)
|
|
foreign := bobCols[0]["id"].(string)
|
|
firstNow := alice.request("GET", "/api/bookmarks/"+first["id"].(string), nil, 200)
|
|
alice.request("PUT", "/api/kanban/cards/"+first["id"].(string), map[string]any{
|
|
"columnId": foreign, "position": 0, "version": firstNow["version"],
|
|
}, 400)
|
|
}
|