133 lines
4.5 KiB
Go
133 lines
4.5 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"home-vue-go/internal/config"
|
|
"home-vue-go/internal/database"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func newAuthTestServer(t *testing.T) (*gin.Engine, *database.Database, *config.Config) {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
cfg := config.New(t.TempDir())
|
|
db, err := database.Init(cfg.DatabasePath, cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := gin.New()
|
|
r.POST("/login", Login(db, cfg))
|
|
r.PUT("/change-password", JWTAuthMiddleware(cfg.JWTSecret), ChangePassword(db))
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
return r, db, cfg
|
|
}
|
|
|
|
func authJSONRequest(t *testing.T, router http.Handler, method, path string, payload any, token string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req := httptest.NewRequest(method, path, strings.NewReader(string(body)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, req)
|
|
return response
|
|
}
|
|
|
|
func tokenFromResponse(t *testing.T, response *httptest.ResponseRecorder) string {
|
|
t.Helper()
|
|
var payload struct {
|
|
Token string `json:"token"`
|
|
}
|
|
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if payload.Token == "" {
|
|
t.Fatalf("login did not return a token: %s", response.Body.String())
|
|
}
|
|
return payload.Token
|
|
}
|
|
|
|
func TestChangePasswordPersistsAndAllowsNewLogin(t *testing.T) {
|
|
router, _, _ := newAuthTestServer(t)
|
|
login := authJSONRequest(t, router, http.MethodPost, "/login", map[string]string{"username": "admin", "password": "admin123"}, "")
|
|
if login.Code != http.StatusOK {
|
|
t.Fatalf("initial login failed: %d %s", login.Code, login.Body.String())
|
|
}
|
|
token := tokenFromResponse(t, login)
|
|
|
|
change := authJSONRequest(t, router, http.MethodPut, "/change-password", map[string]string{
|
|
"oldPassword": "admin123",
|
|
"newPassword": "New-admin-2026!",
|
|
}, token)
|
|
if change.Code != http.StatusOK {
|
|
t.Fatalf("password change failed: %d %s", change.Code, change.Body.String())
|
|
}
|
|
|
|
oldLogin := authJSONRequest(t, router, http.MethodPost, "/login", map[string]string{"username": "admin", "password": "admin123"}, "")
|
|
if oldLogin.Code != http.StatusUnauthorized {
|
|
t.Fatalf("old password should be rejected: %d", oldLogin.Code)
|
|
}
|
|
newLogin := authJSONRequest(t, router, http.MethodPost, "/login", map[string]string{"username": "admin", "password": "New-admin-2026!"}, "")
|
|
if newLogin.Code != http.StatusOK {
|
|
t.Fatalf("new password should work: %d %s", newLogin.Code, newLogin.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestChangePasswordRejectsInvalidInputWithoutLoggingOut(t *testing.T) {
|
|
router, _, _ := newAuthTestServer(t)
|
|
login := authJSONRequest(t, router, http.MethodPost, "/login", map[string]string{"username": "admin", "password": "admin123"}, "")
|
|
token := tokenFromResponse(t, login)
|
|
|
|
weak := authJSONRequest(t, router, http.MethodPut, "/change-password", map[string]string{
|
|
"oldPassword": "admin123",
|
|
"newPassword": "12345678",
|
|
}, token)
|
|
if weak.Code != http.StatusBadRequest {
|
|
t.Fatalf("weak password should be rejected: %d", weak.Code)
|
|
}
|
|
|
|
wrongOld := authJSONRequest(t, router, http.MethodPut, "/change-password", map[string]string{
|
|
"oldPassword": "wrong-password",
|
|
"newPassword": "New-admin-2026!",
|
|
}, token)
|
|
if wrongOld.Code != http.StatusUnprocessableEntity {
|
|
t.Fatalf("wrong current password should be a validation error: %d", wrongOld.Code)
|
|
}
|
|
|
|
stillValid := authJSONRequest(t, router, http.MethodPut, "/change-password", map[string]string{
|
|
"oldPassword": "admin123",
|
|
"newPassword": "New-admin-2026!",
|
|
}, token)
|
|
if stillValid.Code != http.StatusOK {
|
|
t.Fatalf("valid token should remain usable after a rejected attempt: %d %s", stillValid.Code, stillValid.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestValidateNewPassword(t *testing.T) {
|
|
if err := validateNewPassword("admin", "admin123", "New-admin-2026!"); err != nil {
|
|
t.Fatalf("expected valid password: %v", err)
|
|
}
|
|
for _, password := range []string{"short1!", "admin123", "12345678", "lettersonly", "New-admin-2026! "} {
|
|
if err := validateNewPassword("admin", "admin123", password); err == nil {
|
|
t.Errorf("expected password to be rejected: %q", password)
|
|
}
|
|
}
|
|
if err := validateNewPassword("admin", "admin123", strings.Repeat("a1!", 30)); err == nil {
|
|
t.Fatal("expected bcrypt-overlong password to be rejected")
|
|
}
|
|
if err := validateNewPassword("admin", "admin123", "New-管理-2026!"); err != nil {
|
|
t.Fatalf("expected unicode password to be valid: %v", err)
|
|
}
|
|
}
|