package test import ( "RenewCMS/api/handlers/auth" "RenewCMS/api/pkg/apperr" 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 newCancelRegistrationRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo, token *jwtauth.JWTAuth) *chi.Mux { hasher := &stubHasher{} loginHandler := auth.NewLoginHandler( token, useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder), useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder), true, ) verificationHandler := auth.NewVerificationHandler( useCases.NewVerifyUserUseCase(userRepo, hasher, discardRecorder), useCases.NewCancelRegistrationUseCase(userRepo), true, ) r := chi.NewRouter() r.Route("/v1", func(r chi.Router) { r.Route("/auth", func(r chi.Router) { r.Post("/register", loginHandler.Register) r.Group(func(r chi.Router) { r.Use(jwtauth.Verifier(token)) r.Use(jwtauth.Authenticator(token)) r.Post("/verify", verificationHandler.Verify) r.Delete("/register", verificationHandler.CancelRegistration) }) }) }) return r } func newCancelRegistrationTestUserRepo(isVerified bool) *mockUserRepo { repo := newMockUserRepo() repo.users[7] = user.User{ ID: 7, Username: "alice", Email: "alice@example.com", IsVerified: isVerified, } repo.nextID = 7 return repo } func sendCancelRegistrationRequest(router *chi.Mux, token *jwtauth.JWTAuth, claims map[string]any) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodDelete, "/v1/auth/register", nil) if claims != nil { _, tokenString, _ := token.Encode(claims) req.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString}) } rec := httptest.NewRecorder() router.ServeHTTP(rec, req) return rec } func jwtSetCookie(t *testing.T, rec *httptest.ResponseRecorder) (*http.Cookie, bool) { t.Helper() for _, value := range rec.Result().Header.Values("Set-Cookie") { cookie, err := http.ParseSetCookie(value) if err != nil { t.Fatalf("failed to parse Set-Cookie header %q: %v", value, err) } if cookie.Name == "jwt" { return cookie, true } } return nil, false } func TestCancelPendingRegistrationHttp(t *testing.T) { validator.InitValidator() token := jwtauth.New("HS256", []byte("test-secret"), nil) t.Run("An unverified user cancels their pending registration", func(t *testing.T) { repo := newCancelRegistrationTestUserRepo(false) router := newCancelRegistrationRouter(repo, newMockMailRepo(), token) rec := sendCancelRegistrationRequest(router, token, map[string]any{"user_id": uint32(7)}) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) } var body map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("failed to decode response: %v", err) } if _, ok := body["message"]; !ok { t.Errorf("expected a message field in the response body") } cookie, ok := jwtSetCookie(t, rec) if !ok { t.Fatalf("expected a jwt Set-Cookie header") } if cookie.Value != "" || cookie.MaxAge >= 0 { t.Errorf("expected the jwt cookie to be cleared, got %+v", cookie) } if _, err := repo.Get(7); err == nil { t.Errorf("expected user 7 to no longer exist") } else if err != user.ErrUserNotFound { t.Errorf("expected user.ErrUserNotFound, got %v", err) } }) t.Run("Cancelling frees the username and email for a new registration", func(t *testing.T) { repo := newCancelRegistrationTestUserRepo(false) router := newCancelRegistrationRouter(repo, newMockMailRepo(), token) cancelRec := sendCancelRegistrationRequest(router, token, map[string]any{"user_id": uint32(7)}) if cancelRec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", cancelRec.Code, cancelRec.Body.String()) } registerRec := registerRequest(t, router, "alice", "hunter2example12", "alice@example.com") if registerRec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", registerRec.Code, registerRec.Body.String()) } if _, ok := jwtSetCookie(t, registerRec); !ok { t.Fatalf("expected a jwt Set-Cookie header on registration") } newUser, err := repo.GetByUsername("alice") if err != nil { t.Fatalf("expected alice to be registered again: %v", err) } if newUser.ID == 7 { t.Errorf("expected a new id different from 7, got %d", newUser.ID) } }) t.Run("An already-verified user cannot cancel their account this way", func(t *testing.T) { repo := newCancelRegistrationTestUserRepo(true) router := newCancelRegistrationRouter(repo, newMockMailRepo(), token) rec := sendCancelRegistrationRequest(router, token, map[string]any{"user_id": uint32(7)}) if rec.Code != http.StatusForbidden { t.Fatalf("expected status 403, got %d: %s", rec.Code, rec.Body.String()) } if rec.Body.String() != apperr.AlreadyVerifiedErrorMessage+"\n" { t.Errorf("unexpected body: %q", rec.Body.String()) } if _, ok := jwtSetCookie(t, rec); ok { t.Errorf("expected no jwt Set-Cookie header") } stored, err := repo.Get(7) if err != nil { t.Fatalf("expected user 7 to still exist: %v", err) } if !stored.IsVerified { t.Errorf("expected stored user to remain verified") } }) t.Run("A token without a valid user_id returns 401 instead of crashing", func(t *testing.T) { cases := []struct { name string claims map[string]any }{ {"no user_id key at all", map[string]any{"role": "user"}}, {"user_id is a string", map[string]any{"user_id": "seven"}}, {"user_id does not match any user", map[string]any{"user_id": uint32(999)}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { repo := newCancelRegistrationTestUserRepo(false) router := newCancelRegistrationRouter(repo, newMockMailRepo(), token) rec := sendCancelRegistrationRequest(router, token, tc.claims) 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()) } if _, ok := jwtSetCookie(t, rec); ok { t.Errorf("expected no jwt Set-Cookie header") } if _, err := repo.Get(7); err != nil { t.Errorf("expected user 7 to still exist: %v", err) } }) } }) t.Run("A repository failure unrelated to verification returns 500", func(t *testing.T) { repo := newCancelRegistrationTestUserRepo(false) repo.fail.failNext("Delete") router := newCancelRegistrationRouter(repo, newMockMailRepo(), token) rec := sendCancelRegistrationRequest(router, token, map[string]any{"user_id": uint32(7)}) if rec.Code != http.StatusInternalServerError { t.Fatalf("expected status 500, got %d: %s", rec.Code, rec.Body.String()) } if rec.Body.String() != "Internal server error\n" { t.Errorf("unexpected body: %q", rec.Body.String()) } }) t.Run("A request with no jwt cookie never reaches the handler", func(t *testing.T) { repo := newCancelRegistrationTestUserRepo(false) router := newCancelRegistrationRouter(repo, newMockMailRepo(), token) rec := sendCancelRegistrationRequest(router, token, nil) if rec.Code != http.StatusUnauthorized { t.Fatalf("expected status 401, got %d: %s", rec.Code, rec.Body.String()) } if _, err := repo.Get(7); err != nil { t.Errorf("expected user 7 to still exist: %v", err) } }) }