Files
nestly/internal/handlers/auth_test.go
T
2026-10-05 07:40:34 +08:00

186 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"context"
"io/fs"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"nestly/internal/auth"
"nestly/internal/models"
"nestly/internal/templates"
"nestly/web"
)
// newTestServer 建立以記憶體 SQLite 為後端的測試路由與示範帳號。
func newTestServer(t *testing.T) http.Handler {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("開啟記憶體資料庫失敗: %v", err)
}
accounts := models.NewAccountStore(db)
if err := accounts.AutoMigrate(context.Background()); err != nil {
t.Fatalf("自動遷移失敗: %v", err)
}
acct := &models.Account{Email: "demo@nestly.test", Name: "示範帳號", Role: models.RoleMember}
if err := acct.SetPassword("nestly1234"); err != nil {
t.Fatalf("設定密碼失敗: %v", err)
}
if err := accounts.Create(context.Background(), acct); err != nil {
t.Fatalf("建立帳號失敗: %v", err)
}
templateFS, err := fs.Sub(web.TemplatesFS, "templates")
if err != nil {
t.Fatalf("建立模板子目錄失敗: %v", err)
}
engine, err := templates.New(templateFS)
if err != nil {
t.Fatalf("載入模板失敗: %v", err)
}
return NewRouter(Dependencies{
Templates: engine,
Accounts: accounts,
Sessions: auth.NewSessionManager("test-secret", false),
})
}
func TestLoginPageRenders(t *testing.T) {
srv := newTestServer(t)
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/login", nil))
if rec.Code != http.StatusOK {
t.Fatalf("GET /login 狀態碼 = %d, 想要 200", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{"登入", `name="email"`, `name="password"`} {
if !strings.Contains(body, want) {
t.Errorf("GET /login 回應缺少 %q", want)
}
}
}
func TestLoginWrongPassword(t *testing.T) {
srv := newTestServer(t)
form := url.Values{"email": {"demo@nestly.test"}, "password": {"wrong-password"}}
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("POST /login(錯誤密碼)狀態碼 = %d, 想要 401", rec.Code)
}
if body := rec.Body.String(); !strings.Contains(body, "Email 或密碼不正確") {
t.Error("錯誤密碼應顯示錯誤訊息")
}
if body := rec.Body.String(); !strings.Contains(body, `value="demo@nestly.test"`) {
t.Error("錯誤後應保留使用者輸入的 Email")
}
}
func TestLoginSuccessFlow(t *testing.T) {
srv := newTestServer(t)
form := url.Values{"email": {"Demo@Nestly.test "}, "password": {"nestly1234"}} // email 大小寫與空白應被容忍
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, req)
if rec.Code != http.StatusSeeOther {
t.Fatalf("POST /login(正確)狀態碼 = %d, 想要 303", rec.Code)
}
if loc := rec.Header().Get("Location"); loc != "/" {
t.Errorf("登入成功應導向 /, 實際 %q", loc)
}
var cookie *http.Cookie
for _, c := range rec.Result().Cookies() {
if c.Name == auth.CookieName {
cookie = c
}
}
if cookie == nil {
t.Fatal("登入成功應設定 session cookie")
}
// 帶著 session cookie 造訪首頁,應顯示帳號名稱而非登入連結。
req = httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(cookie)
rec = httptest.NewRecorder()
srv.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("GET /(已登入)狀態碼 = %d, 想要 200", rec.Code)
}
if body := rec.Body.String(); !strings.Contains(body, "示範帳號") {
t.Error("已登入的首頁應顯示帳號名稱")
}
// 已登入者造訪 /login 應被導回首頁。
req = httptest.NewRequest(http.MethodGet, "/login", nil)
req.AddCookie(cookie)
rec = httptest.NewRecorder()
srv.ServeHTTP(rec, req)
if rec.Code != http.StatusSeeOther {
t.Errorf("GET /login(已登入)狀態碼 = %d, 想要 303", rec.Code)
}
// 登出後 cookie 失效,首頁不再顯示帳號。
req = httptest.NewRequest(http.MethodPost, "/logout", nil)
req.AddCookie(cookie)
rec = httptest.NewRecorder()
srv.ServeHTTP(rec, req)
var cleared *http.Cookie
for _, c := range rec.Result().Cookies() {
if c.Name == auth.CookieName {
cleared = c
}
}
if cleared == nil || cleared.MaxAge >= 0 {
t.Fatal("登出應清除 session cookie")
}
req = httptest.NewRequest(http.MethodGet, "/", nil)
if cleared != nil {
req.AddCookie(cleared)
}
rec = httptest.NewRecorder()
srv.ServeHTTP(rec, req)
if body := rec.Body.String(); strings.Contains(body, "示範帳號") {
t.Error("登出後首頁不應顯示帳號名稱")
}
}
func TestTamperedSessionRejected(t *testing.T) {
srv := newTestServer(t)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: auth.CookieName, Value: "1.9999999999.deadbeef"})
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, req)
if body := rec.Body.String(); strings.Contains(body, "示範帳號") {
t.Error("偽造的 session 不應被接受")
}
}
func TestStaticCSSServed(t *testing.T) {
srv := newTestServer(t)
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/static/css/app.css", nil))
if rec.Code != http.StatusOK {
t.Fatalf("GET /static/css/app.css 狀態碼 = %d, 想要 200(記得先執行 make css)", rec.Code)
}
}