446 lines
13 KiB
Go
446 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"xtablo-backend/internal/web/handlers"
|
|
)
|
|
|
|
func TestRootRedirectsToLoginWhenUnauthenticated(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router := newTestRouter()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected status 303, got %d", rec.Code)
|
|
}
|
|
if location := rec.Header().Get("Location"); location != "/login" {
|
|
t.Fatalf("expected redirect to /login, got %q", location)
|
|
}
|
|
}
|
|
|
|
func TestUnknownRouteRedirectsToLoginWhenUnauthenticated(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/missing", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router := newTestRouter()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected status 303, got %d", rec.Code)
|
|
}
|
|
if location := rec.Header().Get("Location"); location != "/login" {
|
|
t.Fatalf("expected redirect to /login, got %q", location)
|
|
}
|
|
}
|
|
|
|
func TestLoginPageRenders(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/login", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router := newTestRouter()
|
|
router.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{
|
|
"Se connecter à Xtablo",
|
|
`hx-post="/login"`,
|
|
"https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta2/dist/htmx.min.js",
|
|
`href="/pwa-icons/favicon-32x32.png"`,
|
|
`href="/pwa-icons/favicon-16x16.png"`,
|
|
`href="/pwa-icons/apple-touch-icon-180x180.png"`,
|
|
`href="/manifest.webmanifest"`,
|
|
`src="/logo_dark.png"`,
|
|
`src="/logo_white.png"`,
|
|
`data-testid="auth-card-shell"`,
|
|
"Découvrez la nouvelle expérience de connexion",
|
|
"Mot de passe oublié ?",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected body to contain %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSignupPageRenders(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/signup", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router := newTestRouter()
|
|
router.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{
|
|
"S'inscrire à Xtablo",
|
|
`hx-post="/signup"`,
|
|
"Vous avez déjà un compte ?",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected body to contain %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBrandingAssetsAreServed(t *testing.T) {
|
|
testCases := []string{
|
|
"/logo_dark.png",
|
|
"/logo_white.png",
|
|
"/pwa-icons/favicon-32x32.png",
|
|
"/pwa-icons/favicon-16x16.png",
|
|
"/pwa-icons/apple-touch-icon-180x180.png",
|
|
"/manifest.webmanifest",
|
|
}
|
|
|
|
router := newTestRouter()
|
|
|
|
for _, path := range testCases {
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected %s to return 200, got %d", path, rec.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoginReturnsValidationError(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("email", "")
|
|
form.Set("password", "")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
|
|
router := newTestRouter()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected status 422, got %d", rec.Code)
|
|
}
|
|
|
|
if !strings.Contains(rec.Body.String(), "Veuillez renseigner votre email et votre mot de passe") {
|
|
t.Fatalf("expected validation error fragment, got %q", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestLoginCreatesSessionAndRedirects(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("email", "demo@xtablo.com")
|
|
form.Set("password", "xtablo-demo")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
|
|
router := newTestRouter()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
|
}
|
|
if redirect := rec.Header().Get("HX-Redirect"); redirect != "/" {
|
|
t.Fatalf("expected HX-Redirect to /, got %q", redirect)
|
|
}
|
|
sessionCookie := findCookie(rec.Result().Cookies(), "xtablo_session")
|
|
if sessionCookie == nil {
|
|
t.Fatalf("expected session cookie to be set")
|
|
}
|
|
|
|
homeReq := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
homeReq.AddCookie(sessionCookie)
|
|
homeRec := httptest.NewRecorder()
|
|
router.ServeHTTP(homeRec, homeReq)
|
|
|
|
if homeRec.Code != http.StatusOK {
|
|
t.Fatalf("expected authenticated root status 200, got %d", homeRec.Code)
|
|
}
|
|
for _, want := range []string{
|
|
"Bonjour,",
|
|
"Aperçu",
|
|
"https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta2/dist/htmx.min.js",
|
|
`hx-get="/tasks"`,
|
|
`hx-target="#app-main-content"`,
|
|
`hx-swap="outerHTML"`,
|
|
`hx-push-url="true"`,
|
|
"Tâches",
|
|
"Projets",
|
|
"Planning",
|
|
"Discussions",
|
|
"Fichiers",
|
|
"Feedback",
|
|
"Arctic Matrix",
|
|
"Créer un projet",
|
|
"Créer une tâche",
|
|
"Inviter l'équipe",
|
|
"Envoyer un message",
|
|
"Mes Projets",
|
|
"Mes Tâches",
|
|
`action="/logout"`,
|
|
} {
|
|
if !strings.Contains(homeRec.Body.String(), want) {
|
|
t.Fatalf("expected authenticated home page to contain %q, got %q", want, homeRec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTasksPageRendersFullDashboardPage(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("email", "demo@xtablo.com")
|
|
form.Set("password", "xtablo-demo")
|
|
|
|
loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
|
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
loginRec := httptest.NewRecorder()
|
|
|
|
router := newTestRouter()
|
|
router.ServeHTTP(loginRec, loginReq)
|
|
|
|
sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
|
|
if sessionCookie == nil {
|
|
t.Fatalf("expected session cookie to be set")
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/tasks", nil)
|
|
req.AddCookie(sessionCookie)
|
|
rec := httptest.NewRecorder()
|
|
router.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{
|
|
`class="sidebar-nav-shell"`,
|
|
`id="app-main-content"`,
|
|
"Tâches",
|
|
"Suivez les tâches de votre équipe",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected tasks page to contain %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTasksPageReturnsHTMXMainContentSwap(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("email", "demo@xtablo.com")
|
|
form.Set("password", "xtablo-demo")
|
|
|
|
loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
|
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
loginRec := httptest.NewRecorder()
|
|
|
|
router := newTestRouter()
|
|
router.ServeHTTP(loginRec, loginReq)
|
|
|
|
sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
|
|
if sessionCookie == nil {
|
|
t.Fatalf("expected session cookie to be set")
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/tasks", nil)
|
|
req.Header.Set("HX-Request", "true")
|
|
req.AddCookie(sessionCookie)
|
|
rec := httptest.NewRecorder()
|
|
router.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{
|
|
`id="app-main-content"`,
|
|
`hx-swap-oob="outerHTML"`,
|
|
`id="sidebar-nav-tasks"`,
|
|
"Tâches",
|
|
"Suivez les tâches de votre équipe",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected HTMX tasks response to contain %q", want)
|
|
}
|
|
}
|
|
if strings.Contains(body, `class="sidebar-nav-shell"`) {
|
|
t.Fatalf("expected HTMX tasks response to avoid rerendering the full sidebar")
|
|
}
|
|
}
|
|
|
|
func TestSignupCreatesUserSessionAndRedirects(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("email", "new@xtablo.com")
|
|
form.Set("password", "xtablo-secret")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/signup", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
|
|
router := newTestRouter()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
|
}
|
|
if redirect := rec.Header().Get("HX-Redirect"); redirect != "/" {
|
|
t.Fatalf("expected HX-Redirect to /, got %q", redirect)
|
|
}
|
|
sessionCookie := findCookie(rec.Result().Cookies(), "xtablo_session")
|
|
if sessionCookie == nil {
|
|
t.Fatalf("expected session cookie to be set")
|
|
}
|
|
|
|
loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
|
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
loginRec := httptest.NewRecorder()
|
|
router.ServeHTTP(loginRec, loginReq)
|
|
if loginRec.Header().Get("HX-Redirect") != "/" {
|
|
t.Fatalf("expected signed up user to be able to log in")
|
|
}
|
|
}
|
|
|
|
func TestLogoutClearsSessionAndRedirectsToLogin(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("email", "demo@xtablo.com")
|
|
form.Set("password", "xtablo-demo")
|
|
|
|
loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
|
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
loginRec := httptest.NewRecorder()
|
|
|
|
router := newTestRouter()
|
|
router.ServeHTTP(loginRec, loginReq)
|
|
|
|
sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
|
|
if sessionCookie == nil {
|
|
t.Fatalf("expected session cookie to be set")
|
|
}
|
|
|
|
logoutReq := httptest.NewRequest(http.MethodPost, "/logout", nil)
|
|
logoutReq.AddCookie(sessionCookie)
|
|
logoutRec := httptest.NewRecorder()
|
|
router.ServeHTTP(logoutRec, logoutReq)
|
|
|
|
if logoutRec.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected logout status 303, got %d", logoutRec.Code)
|
|
}
|
|
if location := logoutRec.Header().Get("Location"); location != "/login" {
|
|
t.Fatalf("expected logout redirect to /login, got %q", location)
|
|
}
|
|
|
|
clearedCookie := findCookie(logoutRec.Result().Cookies(), "xtablo_session")
|
|
if clearedCookie == nil {
|
|
t.Fatalf("expected cleared session cookie")
|
|
}
|
|
if clearedCookie.MaxAge >= 0 && clearedCookie.Value != "" {
|
|
t.Fatalf("expected cleared session cookie to be expired, got %+v", clearedCookie)
|
|
}
|
|
|
|
homeReq := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
homeReq.AddCookie(sessionCookie)
|
|
homeRec := httptest.NewRecorder()
|
|
router.ServeHTTP(homeRec, homeReq)
|
|
|
|
if homeRec.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected logged-out root access to redirect, got %d", homeRec.Code)
|
|
}
|
|
if location := homeRec.Header().Get("Location"); location != "/login" {
|
|
t.Fatalf("expected logged-out root redirect to /login, got %q", location)
|
|
}
|
|
}
|
|
|
|
func TestUnknownRouteShowsDashboard404WhenAuthenticated(t *testing.T) {
|
|
form := url.Values{}
|
|
form.Set("email", "demo@xtablo.com")
|
|
form.Set("password", "xtablo-demo")
|
|
|
|
loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
|
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
loginRec := httptest.NewRecorder()
|
|
|
|
router := newTestRouter()
|
|
router.ServeHTTP(loginRec, loginReq)
|
|
|
|
sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
|
|
if sessionCookie == nil {
|
|
t.Fatalf("expected session cookie to be set")
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/missing", nil)
|
|
req.AddCookie(sessionCookie)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected status 404, got %d", rec.Code)
|
|
}
|
|
|
|
body := rec.Body.String()
|
|
for _, want := range []string{
|
|
"Aperçu",
|
|
"Page introuvable",
|
|
"Cette page n'existe pas",
|
|
`href="/"`,
|
|
`action="/logout"`,
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("expected authenticated 404 page to contain %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionSurvivesHandlerRecreation(t *testing.T) {
|
|
repo := handlers.NewInMemoryAuthRepository()
|
|
|
|
loginRouter := newRouterWithHandler(handlers.NewAuthHandler(repo))
|
|
|
|
form := url.Values{}
|
|
form.Set("email", "demo@xtablo.com")
|
|
form.Set("password", "xtablo-demo")
|
|
|
|
loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
|
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
loginRec := httptest.NewRecorder()
|
|
loginRouter.ServeHTTP(loginRec, loginReq)
|
|
|
|
sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
|
|
if sessionCookie == nil {
|
|
t.Fatalf("expected session cookie to be set")
|
|
}
|
|
|
|
reloadedRouter := newRouterWithHandler(handlers.NewAuthHandler(repo))
|
|
|
|
homeReq := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
homeReq.AddCookie(sessionCookie)
|
|
homeRec := httptest.NewRecorder()
|
|
reloadedRouter.ServeHTTP(homeRec, homeReq)
|
|
|
|
if homeRec.Code != http.StatusOK {
|
|
t.Fatalf("expected session to survive handler recreation, got status %d", homeRec.Code)
|
|
}
|
|
}
|
|
|
|
func findCookie(cookies []*http.Cookie, name string) *http.Cookie {
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == name {
|
|
return cookie
|
|
}
|
|
}
|
|
return nil
|
|
}
|