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) } }