package test import ( "RenewCMS/api/handlers/auth" "RenewCMS/api/middleware" "RenewCMS/api/pkg/apperr" "RenewCMS/api/pkg/publicuser" validator "RenewCMS/api/pkg/validator" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/useCases" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/go-chi/jwtauth/v5" ) func newGetCurrentUserRouter(userRepo *mockUserRepo, hasher *stubHasher, token *jwtauth.JWTAuth) *chi.Mux { getUserUseCase := useCases.NewGetUserUseCase(userRepo) meHandler := auth.NewMeHandler(getUserUseCase, useCases.NewGetMyAuthorUseCase(plainAuthors())) verificationHandler := auth.NewVerificationHandler( useCases.NewVerifyUserUseCase(userRepo, hasher, discardRecorder), useCases.NewCancelRegistrationUseCase(userRepo), true, ) r := chi.NewRouter() r.Route("/v1/auth", func(r chi.Router) { r.Group(func(r chi.Router) { r.Use(jwtauth.Verifier(token)) r.Use(jwtauth.Authenticator(token)) r.Use(middleware.RequireCurrentToken(getUserUseCase)) r.Get("/me", meHandler.Me) r.Delete("/register", verificationHandler.CancelRegistration) }) }) return r } func mintGetCurrentUserCookie(t *testing.T, token *jwtauth.JWTAuth, userId uint32, tokenVersion uint32) *http.Cookie { t.Helper() _, tokenString, err := token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion}) if err != nil { t.Fatalf("failed to encode jwt: %v", err) } return &http.Cookie{Name: "jwt", Value: tokenString} } func sendGetCurrentUserRequest(router *chi.Mux, method, target string, cookie *http.Cookie) *httptest.ResponseRecorder { req := httptest.NewRequest(method, target, nil) if cookie != nil { req.AddCookie(cookie) } rec := httptest.NewRecorder() router.ServeHTTP(rec, req) return rec } func TestGetCurrentUserHttp(t *testing.T) { validator.InitValidator() t.Run("A verified user fetches their own profile", func(t *testing.T) { token := jwtauth.New("HS256", []byte("test-secret"), nil) userRepo := newMockUserRepo() userRepo.users[7] = user.User{ ID: 7, Username: "alice", Email: "alice@example.com", IsVerified: true, TokenVersion: 0, } router := newGetCurrentUserRouter(userRepo, &stubHasher{}, token) cookie := mintGetCurrentUserCookie(t, token, 7, 0) rec := sendGetCurrentUserRequest(router, http.MethodGet, "/v1/auth/me", cookie) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) } if got := rec.Header().Get("Cache-Control"); got != "no-store" { t.Errorf("expected Cache-Control no-store, got %q", got) } var body publicuser.PublicUser if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("failed to unmarshal body: %v", err) } if body.ID != 7 || body.Username != "alice" || body.Email != "alice@example.com" || !body.IsVerified { t.Errorf("unexpected body: %+v", body) } }) t.Run("An unverified user still fetches their own profile, with is_verified false, not a 403", func(t *testing.T) { token := jwtauth.New("HS256", []byte("test-secret"), nil) userRepo := newMockUserRepo() userRepo.users[8] = user.User{ ID: 8, Username: "bob", Email: "bob@example.com", IsVerified: false, TokenVersion: 0, } router := newGetCurrentUserRouter(userRepo, &stubHasher{}, token) cookie := mintGetCurrentUserCookie(t, token, 8, 0) rec := sendGetCurrentUserRequest(router, http.MethodGet, "/v1/auth/me", cookie) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) } if got := rec.Header().Get("Cache-Control"); got != "no-store" { t.Errorf("expected Cache-Control no-store, got %q", got) } var body publicuser.PublicUser if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("failed to unmarshal body: %v", err) } if body.IsVerified { t.Errorf("expected is_verified false, got true") } }) t.Run("No jwt cookie at all is refused", func(t *testing.T) { token := jwtauth.New("HS256", []byte("test-secret"), nil) userRepo := newMockUserRepo() userRepo.users[7] = user.User{ID: 7, Username: "alice", IsVerified: true, TokenVersion: 0} router := newGetCurrentUserRouter(userRepo, &stubHasher{}, token) rec := sendGetCurrentUserRequest(router, http.MethodGet, "/v1/auth/me", nil) if rec.Code != http.StatusUnauthorized { t.Fatalf("expected status 401, got %d: %s", rec.Code, rec.Body.String()) } }) t.Run("A jwt whose token_version no longer matches the stored user is refused", func(t *testing.T) { token := jwtauth.New("HS256", []byte("test-secret"), nil) userRepo := newMockUserRepo() userRepo.users[7] = user.User{ID: 7, Username: "alice", IsVerified: true, TokenVersion: 1} router := newGetCurrentUserRouter(userRepo, &stubHasher{}, token) cookie := mintGetCurrentUserCookie(t, token, 7, 0) rec := sendGetCurrentUserRequest(router, http.MethodGet, "/v1/auth/me", cookie) if rec.Code != http.StatusUnauthorized { t.Fatalf("expected status 401, got %d: %s", rec.Code, rec.Body.String()) } if rec.Body.String() != apperr.UnauthorizedErrorMessage+"\n" { t.Errorf("unexpected body: %q", rec.Body.String()) } }) t.Run("A jwt minted for a since-deleted account is refused", func(t *testing.T) { token := jwtauth.New("HS256", []byte("test-secret"), nil) userRepo := newMockUserRepo() userRepo.users[7] = user.User{ID: 7, Username: "alice", IsVerified: false, TokenVersion: 0} router := newGetCurrentUserRouter(userRepo, &stubHasher{}, token) cookie := mintGetCurrentUserCookie(t, token, 7, 0) cancelRec := sendGetCurrentUserRequest(router, http.MethodDelete, "/v1/auth/register", cookie) if cancelRec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", cancelRec.Code, cancelRec.Body.String()) } rec := sendGetCurrentUserRequest(router, http.MethodGet, "/v1/auth/me", cookie) if rec.Code != http.StatusUnauthorized { t.Fatalf("expected status 401, got %d: %s", rec.Code, rec.Body.String()) } if rec.Body.String() != apperr.UnauthorizedErrorMessage+"\n" { t.Errorf("unexpected body: %q", rec.Body.String()) } }) t.Run("The route is never rate-limited, unlike login and password reset", func(t *testing.T) { token := jwtauth.New("HS256", []byte("test-secret"), nil) userRepo := newMockUserRepo() userRepo.users[7] = user.User{ID: 7, Username: "alice", IsVerified: true, TokenVersion: 0} router := newGetCurrentUserRouter(userRepo, &stubHasher{}, token) cookie := mintGetCurrentUserCookie(t, token, 7, 0) for i := 0; i < 5; i++ { rec := sendGetCurrentUserRequest(router, http.MethodGet, "/v1/auth/me", cookie) if rec.Code != http.StatusOK { t.Fatalf("request %d: expected status 200, got %d: %s", i, rec.Code, rec.Body.String()) } if rec.Code == http.StatusTooManyRequests { t.Fatalf("request %d: got 429", i) } } }) }