This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user