forked from alterminal/alterminal
first commit
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoginRequestValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in loginRequest
|
||||
wantErr string // 空字串表示應通過
|
||||
}{
|
||||
{"最小欄位", loginRequest{Username: "alice", Password: "sup3r-secret"}, ""},
|
||||
{"username 帶首尾空白", loginRequest{Username: " alice ", Password: "sup3r-secret"}, ""},
|
||||
{"缺 username", loginRequest{Password: "sup3r-secret"}, "username"},
|
||||
{"username 僅空白", loginRequest{Username: " ", Password: "sup3r-secret"}, "username"},
|
||||
{"缺 password", loginRequest{Username: "alice"}, "password"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.in.validate()
|
||||
if tt.wantErr == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("validate() = %v, want nil", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("validate() = %v, want error containing %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginRequestValidateTrimsUsername(t *testing.T) {
|
||||
in := loginRequest{Username: " alice\t", Password: "sup3r-secret"}
|
||||
if err := in.validate(); err != nil {
|
||||
t.Fatal("validate: ", err)
|
||||
}
|
||||
if in.Username != "alice" {
|
||||
t.Fatalf("validate 後 username = %q, want %q", in.Username, "alice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRandomToken(t *testing.T) {
|
||||
for _, n := range []int{16, 32} {
|
||||
wantLen := (n*8 + 5) / 6 // base64url 無填充的編碼長度
|
||||
seen := make(map[string]bool)
|
||||
for i := 0; i < 100; i++ {
|
||||
token, err := NewToken(n)
|
||||
if err != nil {
|
||||
t.Fatal("NewToken: ", err)
|
||||
}
|
||||
if len(token) != wantLen {
|
||||
t.Fatalf("n=%d token 長度 = %d, want %d", n, len(token), wantLen)
|
||||
}
|
||||
if seen[token] {
|
||||
t.Fatalf("n=%d token 重複: %s", n, token)
|
||||
}
|
||||
seen[token] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 無效請求應在查詢資料庫前就回應,因此 handler 可以傳入 nil db 進行測試。
|
||||
func TestLoginHandlerRejectsInvalidInput(t *testing.T) {
|
||||
h := LoginHandler(nil)
|
||||
plainReq := httptest.NewRequest(http.MethodPost, "/login",
|
||||
strings.NewReader(`{"username":"alice","password":"sup3r-secret"}`))
|
||||
jsonReq := func(body string) *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
req *http.Request
|
||||
wantStatus int
|
||||
}{
|
||||
{"Content-Type 非 JSON", plainReq, http.StatusUnsupportedMediaType},
|
||||
{"JSON 格式錯誤", jsonReq(`{username:`), http.StatusBadRequest},
|
||||
{"缺 username", jsonReq(`{"password":"sup3r-secret"}`), http.StatusBadRequest},
|
||||
{"缺 password", jsonReq(`{"username":"alice"}`), http.StatusBadRequest},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
h(rec, tt.req)
|
||||
if rec.Code != tt.wantStatus {
|
||||
t.Fatalf("status = %d, want %d, body = %s", rec.Code, tt.wantStatus, rec.Body.String())
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "application/json") {
|
||||
t.Fatalf("Content-Type = %q, want application/json", ct)
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), `"error"`) {
|
||||
t.Fatalf("回應應為 JSON error 格式: %s", rec.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPublicUserOmitsPasswordHash(t *testing.T) {
|
||||
u := &User{ID: 7, Username: "alice", Email: "alice@example.com", Name: "Alice", Role: RoleAdmin, PasswordHash: "$argon2id$secret"}
|
||||
pu := newPublicUser(u)
|
||||
if pu.ID != 7 || pu.Username != "alice" || pu.Email != "alice@example.com" || pu.Name != "Alice" || pu.Role != RoleAdmin {
|
||||
t.Fatalf("newPublicUser() = %+v, 欄位不符", pu)
|
||||
}
|
||||
b, err := json.Marshal(pu)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(b), "argon2") {
|
||||
t.Fatalf("回應不得含密碼雜湊: %s", b)
|
||||
}
|
||||
}
|
||||
|
||||
// 表單登入成功後以 303 導向帳號首頁 /,而非停留在 /login。
|
||||
func TestLoginHandlerFormSuccessRedirectsHome(t *testing.T) {
|
||||
db, err := openTestDB()
|
||||
if err != nil {
|
||||
t.Skipf("資料庫不可用,略過整合測試: %v", err)
|
||||
}
|
||||
suffix, err := NewToken(6)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
u := &User{Username: "login-" + suffix, Email: "login-" + suffix + "@example.com", Name: "Login Test"}
|
||||
if err := u.SetPassword("sup3r-secret"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(u).Error; err != nil {
|
||||
t.Fatalf("create user: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
db.Delete(&Session{}, "user_id = ?", u.ID)
|
||||
db.Delete(&User{}, u.ID)
|
||||
})
|
||||
|
||||
body := "csrf_token=token-A&username=" + u.Username + "&password=sup3r-secret"
|
||||
req := formPost(body, &http.Cookie{Name: CSRFCookieName, Value: "token-A"})
|
||||
rec := httptest.NewRecorder()
|
||||
LoginHandler(db)(rec, req)
|
||||
if rec.Code != http.StatusSeeOther {
|
||||
t.Fatalf("status = %d, want 303, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if loc := rec.Header().Get("Location"); loc != "/" {
|
||||
t.Fatalf("Location = %q, want /", loc)
|
||||
}
|
||||
if !strings.Contains(rec.Header().Get("Set-Cookie"), CookieName) {
|
||||
t.Fatalf("登入成功應設定 Session Cookie, Set-Cookie = %v", rec.Header().Values("Set-Cookie"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user