xtablo-source/go-backend/internal/web/handlers/tasks_test.go
2026-05-10 23:14:47 +02:00

790 lines
25 KiB
Go

package handlers
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/google/uuid"
tablomodel "xtablo-backend/internal/tablos"
taskmodel "xtablo-backend/internal/tasks"
"xtablo-backend/internal/web/views"
)
func TestGetTasksPageRendersEtapesAndSansEtapeSections(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookie)
userID, ok := handler.currentUserID(req.Context(), req)
if !ok {
t.Fatal("expected authenticated user")
}
tablo := mustCreateOwnedTablo(t, repo, userID)
etape := mustCreateEtape(t, repo, userID, tablo.ID, "Production")
_ = mustCreateTask(t, repo, userID, tablo.ID, &etape.ID, "Cut footage")
_ = mustCreateTask(t, repo, userID, tablo.ID, nil, "Inbox task")
pageReq := httptest.NewRequest(http.MethodGet, "/tasks", nil)
pageReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTasksPage().ServeHTTP(rec, pageReq)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{"Mes Tâches", "Production", "Sans étape", "Inbox task", "Cut footage"} {
if !strings.Contains(body, want) {
t.Fatalf("expected body to contain %q, got %q", want, body)
}
}
}
func TestGetTasksPageDefaultsToKanbanView(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookie)
userID, ok := handler.currentUserID(req.Context(), req)
if !ok {
t.Fatal("expected authenticated user")
}
tablo := mustCreateOwnedTablo(t, repo, userID)
_ = mustCreateTask(t, repo, userID, tablo.ID, nil, "Shell task")
req = httptest.NewRequest(http.MethodGet, "/tasks", nil)
req.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTasksPage().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{
`data-current-view="kanban"`,
`Nouvelle tâche`,
`Tableau`,
`Liste`,
`Roadmap`,
`Calendrier`,
`Bientôt`,
`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4`,
`draggable="true"`,
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"`,
`/tasks/`,
`Supprimer la tâche Shell task`,
} {
if !strings.Contains(body, want) {
t.Fatalf("expected body to contain %q, got %q", want, body)
}
}
}
func TestGetTasksPageIgnoresInvalidQueryState(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/tasks?view=nope&roadmap_mode=nope&status=bad", nil)
req.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTasksPage().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, `data-current-view="kanban"`) {
t.Fatalf("expected normalized kanban view, got %q", body)
}
if strings.Contains(body, `value="bad" selected`) {
t.Fatalf("expected invalid status filter to be dropped, got %q", body)
}
}
func TestTasksListGroupsByStatusAcrossTablos(t *testing.T) {
now := time.Date(2026, 5, 10, 12, 0, 0, 0, time.UTC)
tabloA := tablomodel.Record{ID: uuid.New(), Name: "Alpha", Color: "#111111"}
tabloB := tablomodel.Record{ID: uuid.New(), Name: "Beta", Color: "#222222"}
records := []taskmodel.Record{
{ID: uuid.New(), TabloID: tabloA.ID, Title: "Write copy", Status: taskmodel.StatusTodo, CreatedAt: now},
{ID: uuid.New(), TabloID: tabloB.ID, Title: "Ship docs", Status: taskmodel.StatusTodo, CreatedAt: now.Add(time.Minute)},
{ID: uuid.New(), TabloID: tabloB.ID, Title: "Review launch", Status: taskmodel.StatusInReview, CreatedAt: now.Add(2 * time.Minute)},
}
vm := views.NewTasksPageViewModel(
[]tablomodel.Record{tabloA, tabloB},
records,
nil,
taskmodel.TaskPageState{View: taskmodel.TaskViewList, RoadmapMode: taskmodel.TaskRoadmapModeWeek},
now,
)
if len(vm.List.Groups) != 4 {
t.Fatalf("expected four status groups, got %d", len(vm.List.Groups))
}
if got := len(vm.List.Groups[0].Tasks); got != 2 {
t.Fatalf("expected two todo tasks across tablos, got %d", got)
}
}
func TestTasksRoadmapCreatesPerTabloSansEtapeLanes(t *testing.T) {
now := time.Date(2026, 5, 10, 12, 0, 0, 0, time.UTC)
tabloA := tablomodel.Record{ID: uuid.New(), Name: "Alpha"}
tabloB := tablomodel.Record{ID: uuid.New(), Name: "Beta"}
due := now.AddDate(0, 0, 3)
records := []taskmodel.Record{
{ID: uuid.New(), TabloID: tabloA.ID, Title: "A task", Status: taskmodel.StatusTodo, DueDate: &due, CreatedAt: now},
{ID: uuid.New(), TabloID: tabloB.ID, Title: "B task", Status: taskmodel.StatusTodo, DueDate: &due, CreatedAt: now},
}
vm := views.NewTasksPageViewModel(
[]tablomodel.Record{tabloA, tabloB},
records,
nil,
taskmodel.TaskPageState{View: taskmodel.TaskViewRoadmap, RoadmapMode: taskmodel.TaskRoadmapModeWeek},
now,
)
if len(vm.Roadmap.Lanes) != 2 {
t.Fatalf("expected one Sans étape lane per tablo, got %d", len(vm.Roadmap.Lanes))
}
}
func TestPostTasksCreatesEtape(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookie)
userID, ok := handler.currentUserID(req.Context(), req)
if !ok {
t.Fatal("expected authenticated user")
}
tablo := mustCreateOwnedTablo(t, repo, userID)
form := url.Values{}
form.Set("tablo_id", tablo.ID.String())
form.Set("title", "Launch")
form.Set("status", string(taskmodel.StatusTodo))
form.Set("is_etape", "true")
postReq := httptest.NewRequest(http.MethodPost, "/tasks", strings.NewReader(form.Encode()))
postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
postReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.PostTasks().ServeHTTP(rec, postReq)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
tasks, err := repo.ListTasksByOwner(context.Background(), userID)
if err != nil {
t.Fatalf("list tasks: %v", err)
}
var created *TaskRecord
for i := range tasks {
if tasks[i].Title == "Launch" {
created = &tasks[i]
break
}
}
if created == nil {
t.Fatalf("expected created etape to be persisted")
}
if !created.IsEtape {
t.Fatalf("expected created record to be an etape, got %#v", created)
}
if created.ParentTaskID != nil {
t.Fatalf("expected created etape to have no parent, got %#v", created.ParentTaskID)
}
}
func TestPostTasksRejectsNonEtapeParent(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookie)
userID, ok := handler.currentUserID(req.Context(), req)
if !ok {
t.Fatal("expected authenticated user")
}
tablo := mustCreateOwnedTablo(t, repo, userID)
parentTask := mustCreateTask(t, repo, userID, tablo.ID, nil, "Regular task")
form := url.Values{}
form.Set("tablo_id", tablo.ID.String())
form.Set("title", "Should fail")
form.Set("status", string(taskmodel.StatusTodo))
form.Set("parent_task_id", parentTask.ID.String())
postReq := httptest.NewRequest(http.MethodPost, "/tasks", strings.NewReader(form.Encode()))
postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
postReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.PostTasks().ServeHTTP(rec, postReq)
if rec.Code != http.StatusUnprocessableEntity {
t.Fatalf("expected status 422, got %d", rec.Code)
}
}
func TestGetEditTaskModalRendersCurrentValues(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookie)
userID, ok := handler.currentUserID(req.Context(), req)
if !ok {
t.Fatal("expected authenticated user")
}
assigneeID, err := repo.CreateAuthUser(context.Background(), CreateAuthUserInput{
Email: "modal-assignee@xtablo.com",
EncryptedPassword: "hash",
DisplayName: "modal assignee",
})
if err != nil {
t.Fatalf("create assignee: %v", err)
}
tablo := mustCreateOwnedTablo(t, repo, userID)
task := mustCreateTaskWithAssignee(t, repo, userID, tablo.ID, nil, "Editable", &assigneeID)
editReq := httptest.NewRequest(http.MethodGet, "/tasks/"+task.ID.String()+"/edit", nil)
editReq.SetPathValue("taskID", task.ID.String())
editReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetEditTaskModal().ServeHTTP(rec, editReq)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{"Editable", string(task.Status), assigneeID.String()} {
if !strings.Contains(body, want) {
t.Fatalf("expected edit modal to contain %q, got %q", want, body)
}
}
}
func TestPatchTaskUpdatesEditableFields(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookie)
userID, ok := handler.currentUserID(req.Context(), req)
if !ok {
t.Fatal("expected authenticated user")
}
assigneeID, err := repo.CreateAuthUser(context.Background(), CreateAuthUserInput{
Email: "patch-assignee@xtablo.com",
EncryptedPassword: "hash",
DisplayName: "patch assignee",
})
if err != nil {
t.Fatalf("create assignee: %v", err)
}
tablo := mustCreateOwnedTablo(t, repo, userID)
etape := mustCreateEtape(t, repo, userID, tablo.ID, "Editing")
task := mustCreateTask(t, repo, userID, tablo.ID, nil, "Old title")
form := url.Values{}
form.Set("tablo_id", tablo.ID.String())
form.Set("title", "New title")
form.Set("description", "New description")
form.Set("status", string(taskmodel.StatusInReview))
form.Set("due_date", "2026-05-20")
form.Set("assignee_id", assigneeID.String())
form.Set("parent_task_id", etape.ID.String())
patchReq := httptest.NewRequest(http.MethodPatch, "/tasks/"+task.ID.String(), strings.NewReader(form.Encode()))
patchReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
patchReq.SetPathValue("taskID", task.ID.String())
patchReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.PatchTask().ServeHTTP(rec, patchReq)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
updated, err := repo.GetTaskByID(context.Background(), task.ID, userID)
if err != nil {
t.Fatalf("get updated task: %v", err)
}
if updated.Title != "New title" || updated.Description != "New description" {
t.Fatalf("expected title/description to update, got %#v", updated)
}
if updated.Status != taskmodel.StatusInReview {
t.Fatalf("expected status to update, got %q", updated.Status)
}
if updated.AssigneeID == nil || *updated.AssigneeID != assigneeID {
t.Fatalf("expected assignee to update, got %#v", updated.AssigneeID)
}
if updated.ParentTaskID == nil || *updated.ParentTaskID != etape.ID {
t.Fatalf("expected parent etape to update, got %#v", updated.ParentTaskID)
}
if updated.DueDate == nil || updated.DueDate.Format("2006-01-02") != "2026-05-20" {
t.Fatalf("expected due date to update, got %#v", updated.DueDate)
}
}
func TestPatchTaskRejectsParentEtapeFromAnotherTablo(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookie)
userID, ok := handler.currentUserID(req.Context(), req)
if !ok {
t.Fatal("expected authenticated user")
}
tabloA := mustCreateOwnedTablo(t, repo, userID)
tabloB := mustCreateOwnedTablo(t, repo, userID)
task := mustCreateTask(t, repo, userID, tabloA.ID, nil, "Alpha task")
foreignEtape := mustCreateEtape(t, repo, userID, tabloB.ID, "Beta stage")
form := url.Values{}
form.Set("tablo_id", tabloA.ID.String())
form.Set("title", task.Title)
form.Set("status", string(taskmodel.StatusTodo))
form.Set("parent_task_id", foreignEtape.ID.String())
patchReq := httptest.NewRequest(http.MethodPatch, "/tasks/"+task.ID.String(), strings.NewReader(form.Encode()))
patchReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
patchReq.SetPathValue("taskID", task.ID.String())
patchReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.PatchTask().ServeHTTP(rec, patchReq)
if rec.Code != http.StatusUnprocessableEntity {
t.Fatalf("expected 422 for cross-tablo parent, got %d", rec.Code)
}
}
func TestGetEditTaskModalShowsOnlyEtapesFromSelectedTablo(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookie)
userID, ok := handler.currentUserID(req.Context(), req)
if !ok {
t.Fatal("expected authenticated user")
}
tabloA := mustCreateOwnedTablo(t, repo, userID)
tabloB := mustCreateOwnedTablo(t, repo, userID)
task := mustCreateTask(t, repo, userID, tabloA.ID, nil, "Editable")
etapeA := mustCreateEtape(t, repo, userID, tabloA.ID, "Stage A")
_ = mustCreateEtape(t, repo, userID, tabloB.ID, "Stage B")
editReq := httptest.NewRequest(http.MethodGet, "/tasks/"+task.ID.String()+"/edit", nil)
editReq.SetPathValue("taskID", task.ID.String())
editReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetEditTaskModal().ServeHTTP(rec, editReq)
body := rec.Body.String()
if !strings.Contains(body, etapeA.ID.String()) {
t.Fatalf("expected same-tablo etape in edit form, got %q", body)
}
if strings.Contains(body, "Stage B") {
t.Fatalf("did not expect foreign tablo etape in edit form, got %q", body)
}
}
func TestDeleteTaskSoftDeletesRegularTask(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookie)
userID, ok := handler.currentUserID(req.Context(), req)
if !ok {
t.Fatal("expected authenticated user")
}
tablo := mustCreateOwnedTablo(t, repo, userID)
task := mustCreateTask(t, repo, userID, tablo.ID, nil, "Delete me")
deleteReq := httptest.NewRequest(http.MethodDelete, "/tasks/"+task.ID.String(), nil)
deleteReq.SetPathValue("taskID", task.ID.String())
deleteReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.DeleteTask().ServeHTTP(rec, deleteReq)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
if _, err := repo.GetTaskByID(context.Background(), task.ID, userID); err == nil {
t.Fatal("expected deleted task to become unavailable")
}
}
func TestGetTasksPageRendersListViewAcrossTablos(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookie)
userID, ok := handler.currentUserID(req.Context(), req)
if !ok {
t.Fatal("expected authenticated user")
}
tabloA := mustCreateOwnedTablo(t, repo, userID)
tabloB := mustCreateOwnedTablo(t, repo, userID)
_ = mustCreateTask(t, repo, userID, tabloA.ID, nil, "Alpha task")
_ = mustCreateTask(t, repo, userID, tabloB.ID, nil, "Beta task")
pageReq := httptest.NewRequest(http.MethodGet, "/tasks?view=list", nil)
pageReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTasksPage().ServeHTTP(rec, pageReq)
body := rec.Body.String()
for _, want := range []string{
`data-current-view="list"`,
`Alpha task`,
`Beta task`,
`Liste`,
`À faire`,
`data-task-list`,
`data-status-group="todo"`,
`Tablo`,
`Assignée`,
} {
if !strings.Contains(body, want) {
t.Fatalf("expected body to contain %q, got %q", want, body)
}
}
}
func TestGetTasksPageRendersRoadmapWeekMode(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookie)
userID, ok := handler.currentUserID(req.Context(), req)
if !ok {
t.Fatal("expected authenticated user")
}
tablo := mustCreateOwnedTablo(t, repo, userID)
etape := mustCreateEtape(t, repo, userID, tablo.ID, "Préparation")
task := mustCreateTask(t, repo, userID, tablo.ID, &etape.ID, "Planifier")
due := time.Date(2026, 5, 12, 0, 0, 0, 0, time.UTC)
_, err := repo.UpdateTask(context.Background(), UpdateTaskInput{
ID: task.ID,
OwnerID: userID,
TabloID: tablo.ID,
Title: task.Title,
Description: task.Description,
Status: task.Status,
DueDate: &due,
ParentTaskID: &etape.ID,
})
if err != nil {
t.Fatalf("update task due date: %v", err)
}
pageReq := httptest.NewRequest(http.MethodGet, "/tasks?view=roadmap&roadmap_mode=week", nil)
pageReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTasksPage().ServeHTTP(rec, pageReq)
body := rec.Body.String()
for _, want := range []string{
`data-current-view="roadmap"`,
`Roadmap`,
`Préparation`,
`Planifier`,
`Semaine`,
`Mois`,
`data-task-roadmap`,
`data-roadmap-lane=`,
`data-roadmap-bucket=`,
} {
if !strings.Contains(body, want) {
t.Fatalf("expected body to contain %q, got %q", want, body)
}
}
}
func TestPatchTaskRendersCurrentTaskQueryState(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookie)
userID, ok := handler.currentUserID(req.Context(), req)
if !ok {
t.Fatal("expected authenticated user")
}
tablo := mustCreateOwnedTablo(t, repo, userID)
task := mustCreateTask(t, repo, userID, tablo.ID, nil, "Stateful")
form := url.Values{}
form.Set("tablo_id", tablo.ID.String())
form.Set("title", "Stateful")
form.Set("status", string(taskmodel.StatusDone))
patchReq := httptest.NewRequest(http.MethodPatch, "/tasks/"+task.ID.String()+"?view=list&status=done", strings.NewReader(form.Encode()))
patchReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
patchReq.SetPathValue("taskID", task.ID.String())
patchReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.PatchTask().ServeHTTP(rec, patchReq)
body := rec.Body.String()
if !strings.Contains(body, `data-current-view="list"`) {
t.Fatalf("expected response to preserve current list view, got %q", body)
}
}
func TestInMemoryTasksListExcludesSoftDeletedRows(t *testing.T) {
repo := NewInMemoryAuthRepository()
user, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
if err != nil {
t.Fatalf("expected demo user, got error %v", err)
}
tablo := mustCreateOwnedTablo(t, repo, user.ID)
etape := mustCreateEtape(t, repo, user.ID, tablo.ID, "Etape 1")
task := mustCreateTask(t, repo, user.ID, tablo.ID, &etape.ID, "Task 1")
if err := repo.SoftDeleteTask(context.Background(), task.ID, user.ID); err != nil {
t.Fatalf("soft delete task: %v", err)
}
records, err := repo.ListTasksByTablo(context.Background(), ListTasksByTabloInput{
OwnerID: user.ID,
TabloID: tablo.ID,
})
if err != nil {
t.Fatalf("list tasks: %v", err)
}
if len(records) != 1 || records[0].ID != etape.ID {
t.Fatalf("expected only etape to remain visible, got %#v", records)
}
}
func TestInMemoryDeleteEtapeClearsChildParentID(t *testing.T) {
repo := NewInMemoryAuthRepository()
user, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
if err != nil {
t.Fatalf("expected demo user, got error %v", err)
}
tablo := mustCreateOwnedTablo(t, repo, user.ID)
etape := mustCreateEtape(t, repo, user.ID, tablo.ID, "Launch")
child := mustCreateTask(t, repo, user.ID, tablo.ID, &etape.ID, "Ship copy")
if err := repo.SoftDeleteTask(context.Background(), etape.ID, user.ID); err != nil {
t.Fatalf("delete etape: %v", err)
}
updated, err := repo.GetTaskByID(context.Background(), child.ID, user.ID)
if err != nil {
t.Fatalf("get child task: %v", err)
}
if updated.ParentTaskID != nil {
t.Fatalf("expected child task to move to Sans etape, got parent %v", *updated.ParentTaskID)
}
}
func TestInMemoryEtapeCannotHaveParent(t *testing.T) {
repo := NewInMemoryAuthRepository()
user, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
if err != nil {
t.Fatalf("expected demo user, got error %v", err)
}
tablo := mustCreateOwnedTablo(t, repo, user.ID)
parent := mustCreateEtape(t, repo, user.ID, tablo.ID, "Parent")
_, err = repo.CreateTask(context.Background(), CreateTaskInput{
OwnerID: user.ID,
TabloID: tablo.ID,
Title: "Invalid child etape",
IsEtape: true,
Status: taskmodel.StatusTodo,
ParentTaskID: &parent.ID,
})
if err == nil {
t.Fatal("expected etape with parent to fail")
}
}
func TestInMemoryTaskParentMustBeEtape(t *testing.T) {
repo := NewInMemoryAuthRepository()
user, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
if err != nil {
t.Fatalf("expected demo user, got error %v", err)
}
tablo := mustCreateOwnedTablo(t, repo, user.ID)
taskParent := mustCreateTask(t, repo, user.ID, tablo.ID, nil, "Not an etape")
_, err = repo.CreateTask(context.Background(), CreateTaskInput{
OwnerID: user.ID,
TabloID: tablo.ID,
Title: "Invalid child task",
Status: taskmodel.StatusTodo,
ParentTaskID: &taskParent.ID,
})
if err == nil {
t.Fatal("expected task with non-etape parent to fail")
}
}
func TestInMemoryTaskAssigneePersistsAndCanBeCleared(t *testing.T) {
repo := NewInMemoryAuthRepository()
user, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
if err != nil {
t.Fatalf("expected demo user, got error %v", err)
}
assigneeID, err := repo.CreateAuthUser(context.Background(), CreateAuthUserInput{
Email: "assignee@xtablo.com",
EncryptedPassword: "hash",
DisplayName: "assignee",
})
if err != nil {
t.Fatalf("create assignee: %v", err)
}
tablo := mustCreateOwnedTablo(t, repo, user.ID)
task := mustCreateTaskWithAssignee(t, repo, user.ID, tablo.ID, nil, "Assigned", &assigneeID)
if task.AssigneeID == nil || *task.AssigneeID != assigneeID {
t.Fatalf("expected assignee to persist, got %#v", task.AssigneeID)
}
updated, err := repo.UpdateTask(context.Background(), UpdateTaskInput{
ID: task.ID,
OwnerID: user.ID,
Title: task.Title,
Description: task.Description,
Status: task.Status,
AssigneeID: nil,
DueDate: task.DueDate,
ParentTaskID: task.ParentTaskID,
})
if err != nil {
t.Fatalf("clear assignee: %v", err)
}
if updated.AssigneeID != nil {
t.Fatalf("expected assignee to clear, got %#v", updated.AssigneeID)
}
}
func mustCreateOwnedTablo(t *testing.T, repo *InMemoryAuthRepository, ownerID uuid.UUID) TabloRecord {
t.Helper()
tablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: ownerID,
Name: "Owned Tablo",
Color: "#3B82F6",
Status: TabloStatusTodo,
})
if err != nil {
t.Fatalf("create tablo: %v", err)
}
return tablo
}
func mustCreateEtape(t *testing.T, repo *InMemoryAuthRepository, ownerID uuid.UUID, tabloID uuid.UUID, title string) TaskRecord {
t.Helper()
record, err := repo.CreateTask(context.Background(), CreateTaskInput{
OwnerID: ownerID,
TabloID: tabloID,
Title: title,
IsEtape: true,
Status: taskmodel.StatusTodo,
})
if err != nil {
t.Fatalf("create etape: %v", err)
}
return record
}
func mustCreateTask(t *testing.T, repo *InMemoryAuthRepository, ownerID uuid.UUID, tabloID uuid.UUID, parentTaskID *uuid.UUID, title string) TaskRecord {
t.Helper()
return mustCreateTaskWithAssignee(t, repo, ownerID, tabloID, parentTaskID, title, nil)
}
func mustCreateTaskWithAssignee(t *testing.T, repo *InMemoryAuthRepository, ownerID uuid.UUID, tabloID uuid.UUID, parentTaskID *uuid.UUID, title string, assigneeID *uuid.UUID) TaskRecord {
t.Helper()
record, err := repo.CreateTask(context.Background(), CreateTaskInput{
OwnerID: ownerID,
TabloID: tabloID,
Title: title,
Status: taskmodel.StatusTodo,
ParentTaskID: parentTaskID,
AssigneeID: assigneeID,
})
if err != nil {
t.Fatalf("create task: %v", err)
}
return record
}