From 9f6c7eb0447ad875733685d1103a874212334380 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Fri, 15 May 2026 22:46:14 +0200 Subject: [PATCH] test(09-03): add task etape assignment tests --- backend/internal/web/handlers_tasks_test.go | 313 ++++++++++++++++++++ 1 file changed, 313 insertions(+) diff --git a/backend/internal/web/handlers_tasks_test.go b/backend/internal/web/handlers_tasks_test.go index 5785112..a5c200b 100644 --- a/backend/internal/web/handlers_tasks_test.go +++ b/backend/internal/web/handlers_tasks_test.go @@ -302,6 +302,319 @@ func TestTaskUpdate(t *testing.T) { } } +func TestTaskEditRendersEtapeSelector(t *testing.T) { + pool, cleanup := setupTestDB(t) + defer cleanup() + + ctx := context.Background() + q := sqlc.New(pool) + store := auth.NewStore(q) + router := newTaskTestRouter(q, store) + + user := preInsertUser(t, ctx, q, "taskeditetape@example.com", "correct-horse-12") + tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{ + UserID: user.ID, + Title: "Task Edit Etape Tablo", + Description: pgtype.Text{Valid: false}, + Color: pgtype.Text{Valid: false}, + }) + if err != nil { + t.Fatalf("InsertTablo: %v", err) + } + etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{ + TabloID: tablo.ID, + Title: "Design", + Description: pgtype.Text{Valid: false}, + Position: 100, + }) + if err != nil { + t.Fatalf("InsertEtape: %v", err) + } + task, err := q.InsertTask(ctx, sqlc.InsertTaskParams{ + TabloID: tablo.ID, + Title: "Editable", + Description: pgtype.Text{Valid: false}, + Status: sqlc.TaskStatusTodo, + Position: 100, + EtapeID: pgtype.UUID{Bytes: etape.ID, Valid: true}, + }) + if err != nil { + t.Fatalf("InsertTask: %v", err) + } + + cookieVal, _, err := store.Create(ctx, user.ID) + if err != nil { + t.Fatalf("store.Create: %v", err) + } + sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} + + req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/tasks/"+task.ID.String()+"/edit", nil) + req.Header.Set("HX-Request", "true") + req.AddCookie(sessionCookie) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("GET task edit status = %d; want 200", rec.Code) + } + body := rec.Body.String() + for _, want := range []string{"Etape", "No etape", "Design"} { + if !strings.Contains(body, want) { + t.Errorf("edit form missing %q; body: %.500s", want, body) + } + } +} + +func TestTaskUpdateAssignsAndUnassignsEtape(t *testing.T) { + pool, cleanup := setupTestDB(t) + defer cleanup() + + ctx := context.Background() + q := sqlc.New(pool) + store := auth.NewStore(q) + router := newTaskTestRouter(q, store) + + user := preInsertUser(t, ctx, q, "taskupdateetape@example.com", "correct-horse-12") + tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{ + UserID: user.ID, + Title: "Task Update Etape Tablo", + Description: pgtype.Text{Valid: false}, + Color: pgtype.Text{Valid: false}, + }) + if err != nil { + t.Fatalf("InsertTablo: %v", err) + } + etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{ + TabloID: tablo.ID, + Title: "Design", + Description: pgtype.Text{Valid: false}, + Position: 100, + }) + if err != nil { + t.Fatalf("InsertEtape: %v", err) + } + task, err := q.InsertTask(ctx, sqlc.InsertTaskParams{ + TabloID: tablo.ID, + Title: "Assignable", + Description: pgtype.Text{Valid: false}, + Status: sqlc.TaskStatusInReview, + Position: 300, + }) + if err != nil { + t.Fatalf("InsertTask: %v", err) + } + + cookieVal, _, err := store.Create(ctx, user.ID) + if err != nil { + t.Fatalf("store.Create: %v", err) + } + sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} + csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String(), []*http.Cookie{sessionCookie}) + + postUpdate := func(etapeID string) { + t.Helper() + form := url.Values{ + "title": {"Assignable"}, + "description": {"Updated"}, + "etape_id": {etapeID}, + "_csrf": {csrfToken}, + } + req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks/"+task.ID.String(), strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("HX-Request", "true") + for _, c := range csrfCookies { + req.AddCookie(c) + } + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("POST task update status = %d; want 200; body: %.500s", rec.Code, rec.Body.String()) + } + } + + postUpdate(etape.ID.String()) + assigned, err := q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: task.ID, TabloID: tablo.ID}) + if err != nil { + t.Fatalf("GetTaskByID assigned: %v", err) + } + if !assigned.EtapeID.Valid || assigned.EtapeID.Bytes != etape.ID { + t.Fatalf("assigned etape_id = %+v; want %s", assigned.EtapeID, etape.ID) + } + if assigned.Status != sqlc.TaskStatusInReview || assigned.Position != 300 { + t.Fatalf("status/position changed after assignment: %+v", assigned) + } + + postUpdate("") + unassigned, err := q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: task.ID, TabloID: tablo.ID}) + if err != nil { + t.Fatalf("GetTaskByID unassigned: %v", err) + } + if unassigned.EtapeID.Valid { + t.Fatalf("etape_id valid after unassign; want null") + } +} + +func TestTaskAssignmentRejectsForeignEtape(t *testing.T) { + pool, cleanup := setupTestDB(t) + defer cleanup() + + ctx := context.Background() + q := sqlc.New(pool) + store := auth.NewStore(q) + router := newTaskTestRouter(q, store) + + user := preInsertUser(t, ctx, q, "taskforeignetape@example.com", "correct-horse-12") + firstTablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{ + UserID: user.ID, + Title: "First", + Description: pgtype.Text{Valid: false}, + Color: pgtype.Text{Valid: false}, + }) + if err != nil { + t.Fatalf("InsertTablo first: %v", err) + } + secondTablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{ + UserID: user.ID, + Title: "Second", + Description: pgtype.Text{Valid: false}, + Color: pgtype.Text{Valid: false}, + }) + if err != nil { + t.Fatalf("InsertTablo second: %v", err) + } + foreignEtape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{ + TabloID: secondTablo.ID, + Title: "Foreign", + Description: pgtype.Text{Valid: false}, + Position: 100, + }) + if err != nil { + t.Fatalf("InsertEtape: %v", err) + } + task, err := q.InsertTask(ctx, sqlc.InsertTaskParams{ + TabloID: firstTablo.ID, + Title: "Protected", + Description: pgtype.Text{Valid: false}, + Status: sqlc.TaskStatusTodo, + Position: 100, + }) + if err != nil { + t.Fatalf("InsertTask: %v", err) + } + + cookieVal, _, err := store.Create(ctx, user.ID) + if err != nil { + t.Fatalf("store.Create: %v", err) + } + sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} + csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+firstTablo.ID.String(), []*http.Cookie{sessionCookie}) + + form := url.Values{ + "title": {"Protected"}, + "description": {""}, + "etape_id": {foreignEtape.ID.String()}, + "_csrf": {csrfToken}, + } + req := httptest.NewRequest(http.MethodPost, "/tablos/"+firstTablo.ID.String()+"/tasks/"+task.ID.String(), strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("HX-Request", "true") + for _, c := range csrfCookies { + req.AddCookie(c) + } + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + if rec.Code == http.StatusOK { + t.Fatalf("foreign etape assignment status = 200; want failure") + } + unchanged, err := q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: task.ID, TabloID: firstTablo.ID}) + if err != nil { + t.Fatalf("GetTaskByID: %v", err) + } + if unchanged.EtapeID.Valid { + t.Fatalf("foreign assignment updated task etape_id: %+v", unchanged.EtapeID) + } +} + +func TestTasksTabUnassignedFilter(t *testing.T) { + pool, cleanup := setupTestDB(t) + defer cleanup() + + ctx := context.Background() + q := sqlc.New(pool) + store := auth.NewStore(q) + router := newTaskTestRouter(q, store) + + user := preInsertUser(t, ctx, q, "tasksunassignedfilter@example.com", "correct-horse-12") + tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{ + UserID: user.ID, + Title: "Unassigned Filter", + Description: pgtype.Text{Valid: false}, + Color: pgtype.Text{Valid: false}, + }) + if err != nil { + t.Fatalf("InsertTablo: %v", err) + } + etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{ + TabloID: tablo.ID, + Title: "Design", + Description: pgtype.Text{Valid: false}, + Position: 100, + }) + if err != nil { + t.Fatalf("InsertEtape: %v", err) + } + _, err = q.InsertTask(ctx, sqlc.InsertTaskParams{ + TabloID: tablo.ID, + Title: "Assigned Hidden", + Description: pgtype.Text{Valid: false}, + Status: sqlc.TaskStatusTodo, + Position: 100, + EtapeID: pgtype.UUID{Bytes: etape.ID, Valid: true}, + }) + if err != nil { + t.Fatalf("InsertTask assigned: %v", err) + } + _, err = q.InsertTask(ctx, sqlc.InsertTaskParams{ + TabloID: tablo.ID, + Title: "Unassigned Visible", + Description: pgtype.Text{Valid: false}, + Status: sqlc.TaskStatusDone, + Position: 100, + }) + if err != nil { + t.Fatalf("InsertTask unassigned: %v", err) + } + + cookieVal, _, err := store.Create(ctx, user.ID) + if err != nil { + t.Fatalf("store.Create: %v", err) + } + sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal} + + req := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/tasks?etape=unassigned", nil) + req.Header.Set("HX-Request", "true") + req.AddCookie(sessionCookie) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("GET unassigned filter status = %d; want 200", rec.Code) + } + body := rec.Body.String() + for _, col := range []string{"To do", "In progress", "In review", "Done"} { + if !strings.Contains(body, col) { + t.Errorf("unassigned filter missing column %q", col) + } + } + if !strings.Contains(body, "Unassigned Visible") { + t.Errorf("unassigned filter missing unassigned task; body: %.500s", body) + } + if strings.Contains(body, "Assigned Hidden") { + t.Errorf("unassigned filter includes assigned task; body: %.500s", body) + } +} + // ---- TestTaskReorderCrossColumn (TASK-04) ---- // TestTaskReorderCrossColumn verifies that POST /tablos/{id}/tasks/reorder