505 lines
15 KiB
Go
505 lines
15 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"backend/internal/auth"
|
|
"backend/internal/db/sqlc"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
func newEtapeTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
|
authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
|
|
tabloDeps := TablosDeps{Queries: q}
|
|
taskDeps := TasksDeps{Queries: q}
|
|
etapeDeps := EtapesDeps{Queries: q}
|
|
router, err := NewRouter(stubPinger{}, os.DirFS("./static"), authDeps, tabloDeps, taskDeps, etapeDeps, EventsDeps{Queries: q}, FilesDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
|
if err != nil {
|
|
panic("newEtapeTestRouter: " + err.Error())
|
|
}
|
|
return router
|
|
}
|
|
|
|
func TestEtapeCreateRendersChipAndCount(t *testing.T) {
|
|
pool, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
q := sqlc.New(pool)
|
|
store := auth.NewStore(q)
|
|
router := newEtapeTestRouter(q, store)
|
|
|
|
user := preInsertUser(t, ctx, q, "etapecreate@example.com", "correct-horse-12")
|
|
tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
|
|
UserID: user.ID,
|
|
Title: "Etape Create Tablo",
|
|
Description: pgtype.Text{Valid: false},
|
|
Color: pgtype.Text{Valid: false},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("InsertTablo: %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()+"/tasks", []*http.Cookie{sessionCookie})
|
|
|
|
form := url.Values{
|
|
"title": {"Design"},
|
|
"_csrf": {csrfToken},
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/etapes", 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 /tablos/{id}/etapes status = %d; want 200", rec.Code)
|
|
}
|
|
body := rec.Body.String()
|
|
for _, want := range []string{"Design", "Unassigned", "All"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("response body missing %q; body: %.500s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTaskCreateAssignsEtape(t *testing.T) {
|
|
pool, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
q := sqlc.New(pool)
|
|
store := auth.NewStore(q)
|
|
router := newEtapeTestRouter(q, store)
|
|
|
|
user := preInsertUser(t, ctx, q, "tasketapecreate@example.com", "correct-horse-12")
|
|
tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
|
|
UserID: user.ID,
|
|
Title: "Task Etape Create 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)
|
|
}
|
|
|
|
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()+"/tasks", []*http.Cookie{sessionCookie})
|
|
|
|
form := url.Values{
|
|
"title": {"Assigned Task"},
|
|
"status": {"todo"},
|
|
"etape_id": {etape.ID.String()},
|
|
"_csrf": {csrfToken},
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks", 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 /tablos/{id}/tasks status = %d; want 200", rec.Code)
|
|
}
|
|
tasks, err := q.ListTasksByTablo(ctx, tablo.ID)
|
|
if err != nil {
|
|
t.Fatalf("ListTasksByTablo: %v", err)
|
|
}
|
|
if len(tasks) != 1 {
|
|
t.Fatalf("task count = %d; want 1", len(tasks))
|
|
}
|
|
if !tasks[0].EtapeID.Valid || tasks[0].EtapeID.Bytes != etape.ID {
|
|
t.Fatalf("task etape_id = %+v; want %s", tasks[0].EtapeID, etape.ID)
|
|
}
|
|
}
|
|
|
|
func TestEtapeFilterRendersExistingKanbanColumns(t *testing.T) {
|
|
pool, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
q := sqlc.New(pool)
|
|
store := auth.NewStore(q)
|
|
router := newEtapeTestRouter(q, store)
|
|
|
|
user := preInsertUser(t, ctx, q, "etapefilter@example.com", "correct-horse-12")
|
|
tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
|
|
UserID: user.ID,
|
|
Title: "Etape Filter 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)
|
|
}
|
|
_, err = q.InsertTask(ctx, sqlc.InsertTaskParams{
|
|
TabloID: tablo.ID,
|
|
Title: "Assigned Task",
|
|
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 Task",
|
|
Description: pgtype.Text{Valid: false},
|
|
Status: sqlc.TaskStatusTodo,
|
|
Position: 200,
|
|
EtapeID: pgtype.UUID{Valid: false},
|
|
})
|
|
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="+etape.ID.String(), 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 /tablos/{id}/tasks?etape 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("filtered kanban missing column header %q", col)
|
|
}
|
|
}
|
|
if !strings.Contains(body, "Assigned Task") {
|
|
t.Errorf("filtered body missing assigned task; body: %.500s", body)
|
|
}
|
|
if strings.Contains(body, "Unassigned Task") {
|
|
t.Errorf("filtered body includes unassigned task; body: %.500s", body)
|
|
}
|
|
}
|
|
|
|
func TestEtapeUpdateChangesTitleAndDescription(t *testing.T) {
|
|
pool, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
q := sqlc.New(pool)
|
|
store := auth.NewStore(q)
|
|
router := newEtapeTestRouter(q, store)
|
|
|
|
user := preInsertUser(t, ctx, q, "etapeupdate@example.com", "correct-horse-12")
|
|
tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
|
|
UserID: user.ID,
|
|
Title: "Etape Update 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: "Draft",
|
|
Description: pgtype.Text{String: "Old", Valid: true},
|
|
Position: 100,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("InsertEtape: %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()+"/tasks", []*http.Cookie{sessionCookie})
|
|
|
|
form := url.Values{
|
|
"title": {"Design"},
|
|
"description": {"Ready for build"},
|
|
"_csrf": {csrfToken},
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/etapes/"+etape.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 /tablos/{id}/etapes/{etape_id} status = %d; want 200", rec.Code)
|
|
}
|
|
body := rec.Body.String()
|
|
if !strings.Contains(body, "Design") {
|
|
t.Fatalf("response body missing updated title; body: %.500s", body)
|
|
}
|
|
updated, err := q.GetEtapeByID(ctx, sqlc.GetEtapeByIDParams{ID: etape.ID, TabloID: tablo.ID})
|
|
if err != nil {
|
|
t.Fatalf("GetEtapeByID: %v", err)
|
|
}
|
|
if updated.Title != "Design" || !updated.Description.Valid || updated.Description.String != "Ready for build" {
|
|
t.Fatalf("updated etape = %+v; want new title and description", updated)
|
|
}
|
|
}
|
|
|
|
func TestEtapeDeleteUnassignsTasks(t *testing.T) {
|
|
pool, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
q := sqlc.New(pool)
|
|
store := auth.NewStore(q)
|
|
router := newEtapeTestRouter(q, store)
|
|
|
|
user := preInsertUser(t, ctx, q, "etapedelete@example.com", "correct-horse-12")
|
|
tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
|
|
UserID: user.ID,
|
|
Title: "Etape Delete 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: "Survives",
|
|
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}
|
|
csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/tasks", []*http.Cookie{sessionCookie})
|
|
|
|
form := url.Values{"_csrf": {csrfToken}}
|
|
req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/etapes/"+etape.ID.String()+"/delete", 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 delete etape status = %d; want 200", rec.Code)
|
|
}
|
|
remaining, err := q.GetTaskByID(ctx, sqlc.GetTaskByIDParams{ID: task.ID, TabloID: tablo.ID})
|
|
if err != nil {
|
|
t.Fatalf("GetTaskByID after etape delete: %v", err)
|
|
}
|
|
if remaining.EtapeID.Valid {
|
|
t.Fatalf("task etape_id valid after etape delete; want null")
|
|
}
|
|
}
|
|
|
|
func TestEtapeReorderPersistsPosition(t *testing.T) {
|
|
pool, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
q := sqlc.New(pool)
|
|
store := auth.NewStore(q)
|
|
router := newEtapeTestRouter(q, store)
|
|
|
|
user := preInsertUser(t, ctx, q, "etapereorder@example.com", "correct-horse-12")
|
|
tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
|
|
UserID: user.ID,
|
|
Title: "Etape Reorder Tablo",
|
|
Description: pgtype.Text{Valid: false},
|
|
Color: pgtype.Text{Valid: false},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("InsertTablo: %v", err)
|
|
}
|
|
first, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
|
|
TabloID: tablo.ID,
|
|
Title: "First",
|
|
Description: pgtype.Text{Valid: false},
|
|
Position: 100,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("InsertEtape first: %v", err)
|
|
}
|
|
second, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
|
|
TabloID: tablo.ID,
|
|
Title: "Second",
|
|
Description: pgtype.Text{Valid: false},
|
|
Position: 200,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("InsertEtape second: %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()+"/tasks", []*http.Cookie{sessionCookie})
|
|
|
|
form := url.Values{
|
|
"etape_id": {second.ID.String(), first.ID.String()},
|
|
"_csrf": {csrfToken},
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/etapes/reorder", 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 reorder etapes status = %d; want 200", rec.Code)
|
|
}
|
|
etapes, err := q.ListEtapesByTablo(ctx, tablo.ID)
|
|
if err != nil {
|
|
t.Fatalf("ListEtapesByTablo: %v", err)
|
|
}
|
|
if len(etapes) != 2 || etapes[0].ID != second.ID || etapes[1].ID != first.ID {
|
|
t.Fatalf("etape order = %+v; want second then first", etapes)
|
|
}
|
|
}
|
|
|
|
func TestEtapeOwnershipReturns404(t *testing.T) {
|
|
pool, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
q := sqlc.New(pool)
|
|
store := auth.NewStore(q)
|
|
router := newEtapeTestRouter(q, store)
|
|
|
|
owner := preInsertUser(t, ctx, q, "etapeowner@example.com", "correct-horse-12")
|
|
nonOwner := preInsertUser(t, ctx, q, "etapenonowner@example.com", "correct-horse-12")
|
|
tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
|
|
UserID: owner.ID,
|
|
Title: "Owned Elsewhere",
|
|
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: "Private",
|
|
Description: pgtype.Text{Valid: false},
|
|
Position: 100,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("InsertEtape: %v", err)
|
|
}
|
|
|
|
cookieVal, _, err := store.Create(ctx, nonOwner.ID)
|
|
if err != nil {
|
|
t.Fatalf("store.Create: %v", err)
|
|
}
|
|
sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
|
|
csrfToken, csrfCookies := getCSRFToken(t, router, "/", []*http.Cookie{sessionCookie})
|
|
|
|
cases := []struct {
|
|
name string
|
|
path string
|
|
form url.Values
|
|
}{
|
|
{
|
|
name: "update",
|
|
path: "/tablos/" + tablo.ID.String() + "/etapes/" + etape.ID.String(),
|
|
form: url.Values{"title": {"Nope"}, "_csrf": {csrfToken}},
|
|
},
|
|
{
|
|
name: "delete",
|
|
path: "/tablos/" + tablo.ID.String() + "/etapes/" + etape.ID.String() + "/delete",
|
|
form: url.Values{"_csrf": {csrfToken}},
|
|
},
|
|
{
|
|
name: "reorder",
|
|
path: "/tablos/" + tablo.ID.String() + "/etapes/reorder",
|
|
form: url.Values{"etape_id": {etape.ID.String()}, "_csrf": {csrfToken}},
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, tc.path, strings.NewReader(tc.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.StatusNotFound {
|
|
t.Fatalf("%s status = %d; want 404", tc.name, rec.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|