xtablo-source/go-backend/internal/web/handlers/tablos_test.go

632 lines
17 KiB
Go

package handlers
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
func TestInMemoryTablosListExcludesSoftDeletedRows(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)
}
deletedTablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: user.ID,
Name: "Visible",
Status: TabloStatusTodo,
})
if err != nil {
t.Fatalf("create deleted tablo: %v", err)
}
keptTablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: user.ID,
Name: "Kept",
Status: TabloStatusTodo,
})
if err != nil {
t.Fatalf("create kept tablo: %v", err)
}
if err := repo.SoftDeleteTablo(context.Background(), deletedTablo.ID, user.ID); err != nil {
t.Fatalf("soft delete tablo: %v", err)
}
tablos, err := repo.ListTablos(context.Background(), ListTablosInput{
OwnerID: user.ID,
})
if err != nil {
t.Fatalf("list tablos: %v", err)
}
if len(tablos) != 1 {
t.Fatalf("expected 1 visible tablo, got %d", len(tablos))
}
if tablos[0].ID != keptTablo.ID {
t.Fatalf("expected kept tablo %s, got %s", keptTablo.ID, tablos[0].ID)
}
}
func TestInMemoryTablosListFiltersBySearchAndStatus(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)
}
_, err = repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: user.ID,
Name: "Hello Product",
Status: TabloStatusTodo,
})
if err != nil {
t.Fatalf("create todo tablo: %v", err)
}
expectedTablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: user.ID,
Name: "Hello Delivery",
Status: TabloStatusInProgress,
})
if err != nil {
t.Fatalf("create in progress tablo: %v", err)
}
tablos, err := repo.ListTablos(context.Background(), ListTablosInput{
OwnerID: user.ID,
Query: "delivery",
Status: &[]TabloStatus{TabloStatusInProgress}[0],
})
if err != nil {
t.Fatalf("list filtered tablos: %v", err)
}
if len(tablos) != 1 {
t.Fatalf("expected 1 filtered tablo, got %d", len(tablos))
}
if tablos[0].ID != expectedTablo.ID {
t.Fatalf("expected tablo %s, got %s", expectedTablo.ID, tablos[0].ID)
}
}
func TestInMemoryTablosSoftDeleteRejectsDifferentOwner(t *testing.T) {
repo := NewInMemoryAuthRepository()
owner, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
if err != nil {
t.Fatalf("expected demo user, got error %v", err)
}
otherUserID, err := repo.CreateAuthUser(context.Background(), CreateAuthUserInput{
Email: "other@xtablo.com",
EncryptedPassword: "hash",
DisplayName: "other",
})
if err != nil {
t.Fatalf("create other user: %v", err)
}
tablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: owner.ID,
Name: "Owned",
Status: TabloStatusTodo,
})
if err != nil {
t.Fatalf("create owned tablo: %v", err)
}
if err := repo.SoftDeleteTablo(context.Background(), tablo.ID, otherUserID); err == nil {
t.Fatal("expected deleting another user's tablo to fail")
}
}
func TestGetTablosPageDefaultsToGridAndAllStatus(t *testing.T) {
handler := newTestAuthHandler(t)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/tablos", nil)
req.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTablosPage().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{
"Mes Projets",
"Nouveau projet",
"Vue en grille",
"Rechercher...",
"Tous",
} {
if !strings.Contains(body, want) {
t.Fatalf("expected body to contain %q", want)
}
}
}
func TestGetTablosPageHonorsSearchAndStatus(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 user session")
}
_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: userID,
Name: "Alpha Draft",
Status: TabloStatusTodo,
})
if err != nil {
t.Fatalf("create todo tablo: %v", err)
}
_, err = repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: userID,
Name: "Beta Delivery",
Status: TabloStatusInProgress,
})
if err != nil {
t.Fatalf("create filtered tablo: %v", err)
}
pageReq := httptest.NewRequest(http.MethodGet, "/tablos?q=delivery&status=in_progress", nil)
pageReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTablosPage().ServeHTTP(rec, pageReq)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, "Beta Delivery") {
t.Fatalf("expected filtered tablo to be visible, got %q", body)
}
if strings.Contains(body, "Alpha Draft") {
t.Fatalf("expected non-matching tablo to be filtered out, got %q", body)
}
}
func TestGetTablosPageUsesSharedToolbarButtonAndStatusBadge(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 user session")
}
_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: userID,
Name: "Shared UI",
Status: TabloStatusInProgress,
})
if err != nil {
t.Fatalf("create tablo: %v", err)
}
pageReq := httptest.NewRequest(http.MethodGet, "/tablos", nil)
pageReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTablosPage().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{
`ui-button ui-button-solid ui-button-default ui-button-md`,
`ui-badge ui-badge-warning`,
} {
if !strings.Contains(body, want) {
t.Fatalf("expected shared primitive markup %q, got %q", want, body)
}
}
}
func TestGetTablosPageModalUsesSharedFormPrimitives(t *testing.T) {
handler := newTestAuthHandler(t)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/tablos?modal=create", nil)
req.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTablosPage().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{
`ui-modal-panel`,
`ui-form-field`,
`class="ui-input"`,
} {
if !strings.Contains(body, want) {
t.Fatalf("expected modal primitive markup %q, got %q", want, body)
}
}
}
func TestGetTablosPageListViewRendersTableLayout(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 user session")
}
_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: userID,
Name: "Table View",
Status: TabloStatusInProgress,
})
if err != nil {
t.Fatalf("create tablo: %v", err)
}
pageReq := httptest.NewRequest(http.MethodGet, "/tablos?view=list", nil)
pageReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTablosPage().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{
`class="ui-table-shell"`,
`<table class="ui-table">`,
"Progression",
"Table View",
} {
if !strings.Contains(body, want) {
t.Fatalf("expected list view to contain %q, got %q", want, body)
}
}
}
func TestGetTablosPageEmptyStateUsesSharedPrimitive(t *testing.T) {
handler := newTestAuthHandler(t)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/tablos", nil)
req.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTablosPage().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{
`ui-empty-state`,
`Aucun projet trouvé`,
`Créez votre premier projet`,
} {
if !strings.Contains(body, want) {
t.Fatalf("expected empty state primitive markup %q, got %q", want, body)
}
}
}
func TestGetTablosPageListViewUsesDirectTableIconMarkup(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 user session")
}
_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: userID,
Name: "Markup Check",
Status: TabloStatusInProgress,
})
if err != nil {
t.Fatalf("create tablo: %v", err)
}
pageReq := httptest.NewRequest(http.MethodGet, "/tablos?view=list", nil)
pageReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTablosPage().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{
`class="flex items-center gap-1.5 [&>svg]:w-4 [&>svg]:h-4 [&>svg]:shrink-0"><svg viewBox="0 0 24 24"`,
`class="borderless-icon-button"`,
`class="lucide lucide-trash2 w-4 h-4"`,
} {
if !strings.Contains(body, want) {
t.Fatalf("expected list view markup to contain %q, got %q", want, body)
}
}
}
func TestGetTablosPageGridUsesProjectDateRowMarkup(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 user session")
}
_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: userID,
Name: "Calendar Check",
Status: TabloStatusTodo,
})
if err != nil {
t.Fatalf("create tablo: %v", err)
}
pageReq := httptest.NewRequest(http.MethodGet, "/tablos", nil)
pageReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTablosPage().ServeHTTP(rec, pageReq)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
if !strings.Contains(rec.Body.String(), `class="project-date-row"><svg viewBox="0 0 24 24"`) {
t.Fatalf("expected grid card calendar icon to render inside project-date-row markup, got %q", rec.Body.String())
}
}
func TestGetTablosPageGridUsesProjectCardMarkup(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 user session")
}
_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: userID,
Name: "Hello",
Status: TabloStatusInProgress,
})
if err != nil {
t.Fatalf("create tablo: %v", err)
}
pageReq := httptest.NewRequest(http.MethodGet, "/tablos", nil)
pageReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTablosPage().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{
`<article class="project-card">`,
`class="project-card-top"`,
`class="borderless-icon-button"`,
`class="project-card-title-row"`,
`class="project-avatar project-accent-`,
`class="project-date-row"`,
`class="project-progress-track"`,
} {
if !strings.Contains(body, want) {
t.Fatalf("expected grid card markup to contain %q, got %q", want, body)
}
}
}
func TestPostTablosCreatesTodoTablo(t *testing.T) {
handler := newTestAuthHandler(t)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
form := url.Values{}
form.Set("name", "Roadmap")
form.Set("view", "grid")
form.Set("status", "all")
req := httptest.NewRequest(http.MethodPost, "/tablos", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.PostTablos().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, "Roadmap") {
t.Fatalf("expected created tablo in response, got %q", body)
}
if !strings.Contains(body, "À faire") {
t.Fatalf("expected todo status label in response, got %q", body)
}
}
func TestPostTablosWithEmptyNameReturns422(t *testing.T) {
handler := newTestAuthHandler(t)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
form := url.Values{}
req := httptest.NewRequest(http.MethodPost, "/tablos", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.PostTablos().ServeHTTP(rec, req)
if rec.Code != http.StatusUnprocessableEntity {
t.Fatalf("expected status 422, got %d", rec.Code)
}
if !strings.Contains(rec.Body.String(), "Le nom du projet est requis") {
t.Fatalf("expected validation error, got %q", rec.Body.String())
}
}
func TestDeleteTabloSoftDeletesOwnedRow(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 user session")
}
tablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: userID,
Name: "Disposable",
Status: TabloStatusTodo,
})
if err != nil {
t.Fatalf("create tablo: %v", err)
}
deleteReq := httptest.NewRequest(http.MethodDelete, "/tablos/"+tablo.ID.String(), nil)
deleteReq.SetPathValue("tabloID", tablo.ID.String())
deleteReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.DeleteTablo().ServeHTTP(rec, deleteReq)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
list, err := repo.ListTablos(context.Background(), ListTablosInput{OwnerID: userID})
if err != nil {
t.Fatalf("list tablos: %v", err)
}
if len(list) != 0 {
t.Fatalf("expected deleted tablo to be hidden, got %d rows", len(list))
}
}
func TestDeleteTabloRejectsDifferentOwner(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
ownerCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
ownerReq := httptest.NewRequest(http.MethodGet, "/", nil)
ownerReq.AddCookie(ownerCookie)
ownerID, ok := handler.currentUserID(ownerReq.Context(), ownerReq)
if !ok {
t.Fatal("expected owner session")
}
tablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: ownerID,
Name: "Owned",
Status: TabloStatusTodo,
})
if err != nil {
t.Fatalf("create tablo: %v", err)
}
passwordHash, err := hashPassword("xtablo-demo")
if err != nil {
t.Fatalf("hash password: %v", err)
}
_, err = repo.CreateAuthUser(context.Background(), CreateAuthUserInput{
Email: "other@xtablo.com",
EncryptedPassword: passwordHash,
DisplayName: "other",
})
if err != nil {
t.Fatalf("create user: %v", err)
}
otherCookie := loginTestUser(t, handler, "other@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodDelete, "/tablos/"+tablo.ID.String(), nil)
req.SetPathValue("tabloID", tablo.ID.String())
req.AddCookie(otherCookie)
rec := httptest.NewRecorder()
handler.DeleteTablo().ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected status 404, got %d", rec.Code)
}
}
func loginTestUser(t *testing.T, handler *AuthHandler, email string, password string) *http.Cookie {
t.Helper()
form := url.Values{}
form.Set("email", email)
form.Set("password", password)
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
handler.PostLogin().ServeHTTP(rec, req)
for _, cookie := range rec.Result().Cookies() {
if cookie.Name == "xtablo_session" {
return cookie
}
}
t.Fatal("expected session cookie to be set")
return nil
}