package test import ( "RenewCMS/api/handlers/auth" validator "RenewCMS/api/pkg/validator" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/useCases" "errors" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-chi/chi/v5" "github.com/go-chi/jwtauth/v5" ) func newRegistrationStatusRouter(userRepo *mockUserRepo, mailRepo *mockMailRepo) (*chi.Mux, *useCases.CreateUserUseCase) { token := jwtauth.New("HS256", []byte("test-secret"), nil) hasher := &stubHasher{} createUseCase := useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com", discardRecorder) loginHandler := auth.NewLoginHandler(token, createUseCase, useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder), true) r := chi.NewRouter() r.Route("/v1/auth", func(r chi.Router) { r.Post("/register", loginHandler.Register) r.Get("/registration", loginHandler.RegistrationStatus) }) return r, createUseCase } func registrationStatusRequest(router *chi.Mux, cookie *http.Cookie) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodGet, "/v1/auth/registration", nil) if cookie != nil { req.AddCookie(cookie) } rec := httptest.NewRecorder() router.ServeHTTP(rec, req) return rec } func TestRegistrationStatusEndpointHttp(t *testing.T) { validator.InitValidator() t.Run("Registration is reported open while no verified account exists", func(t *testing.T) { userRepo := newMockUserRepo() mailRepo := newMockMailRepo() router, _ := newRegistrationStatusRouter(userRepo, mailRepo) rec := registrationStatusRequest(router, nil) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) } if rec.Header().Get("Cache-Control") != "no-store" { t.Errorf("expected Cache-Control no-store, got %q", rec.Header().Get("Cache-Control")) } if rec.Body.String() != `{"open":true}` { t.Errorf("expected body {\"open\":true}, got %q", rec.Body.String()) } }) t.Run("Registration is reported open while every existing account is unverified", func(t *testing.T) { userRepo := newMockUserRepo() userRepo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com", IsVerified: false} userRepo.nextID = 1 mailRepo := newMockMailRepo() router, _ := newRegistrationStatusRouter(userRepo, mailRepo) rec := registrationStatusRequest(router, nil) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) } if rec.Body.String() != `{"open":true}` { t.Errorf("expected body {\"open\":true}, got %q", rec.Body.String()) } }) t.Run("Registration is reported closed once a verified account exists", func(t *testing.T) { userRepo := newMockUserRepo() userRepo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com", IsVerified: true} userRepo.nextID = 1 mailRepo := newMockMailRepo() router, _ := newRegistrationStatusRouter(userRepo, mailRepo) rec := registrationStatusRequest(router, nil) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) } if rec.Header().Get("Cache-Control") != "no-store" { t.Errorf("expected Cache-Control no-store, got %q", rec.Header().Get("Cache-Control")) } if rec.Body.String() != `{"open":false}` { t.Errorf("expected body {\"open\":false}, got %q", rec.Body.String()) } }) t.Run("An expired, forged, or otherwise invalid jwt cookie makes no difference to the response", func(t *testing.T) { userRepo := newMockUserRepo() userRepo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com", IsVerified: true} userRepo.nextID = 1 mailRepo := newMockMailRepo() router, _ := newRegistrationStatusRouter(userRepo, mailRepo) invalidCookie := &http.Cookie{Name: "jwt", Value: "not-a-valid-jwt-token"} rec := registrationStatusRequest(router, invalidCookie) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) } if rec.Body.String() != `{"open":false}` { t.Errorf("expected body {\"open\":false}, got %q", rec.Body.String()) } if rec.Header().Get("Set-Cookie") != "" { t.Errorf("expected no cookie to be set or cleared, got %q", rec.Header().Get("Set-Cookie")) } }) t.Run("The endpoint reveals nothing beyond the open flag", func(t *testing.T) { userRepo := newMockUserRepo() userRepo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com", IsVerified: true} userRepo.users[2] = user.User{ID: 2, Username: "bob", Email: "bob@example.com", IsVerified: true} userRepo.users[3] = user.User{ID: 3, Username: "carol", Email: "carol@example.com", IsVerified: false} userRepo.nextID = 3 mailRepo := newMockMailRepo() router, _ := newRegistrationStatusRouter(userRepo, mailRepo) rec := registrationStatusRequest(router, nil) body := rec.Body.String() if body != `{"open":false}` { t.Fatalf("expected body {\"open\":false} with only the open field, got %q", body) } forbidden := []string{"alice", "bob", "carol", "@example.com", "is_verified", "count"} for _, term := range forbidden { if strings.Contains(body, term) { t.Errorf("expected the body to not reveal %q, got %q", term, body) } } }) t.Run("The route is never rate-limited, unlike login and password reset", func(t *testing.T) { userRepo := newMockUserRepo() mailRepo := newMockMailRepo() router, _ := newRegistrationStatusRouter(userRepo, mailRepo) for i := 0; i < 20; i++ { rec := registrationStatusRequest(router, nil) if rec.Code != http.StatusOK { t.Fatalf("request %d: expected status 200, got %d", i+1, rec.Code) } if rec.Code == http.StatusTooManyRequests { t.Fatalf("request %d: unexpectedly rate-limited", i+1) } } }) t.Run("The same rule CreateUser enforces is what answers this endpoint", func(t *testing.T) { userRepo := newMockUserRepo() userRepo.users[1] = user.User{ID: 1, Username: "alice", Email: "alice@example.com", IsVerified: true} userRepo.nextID = 1 mailRepo := newMockMailRepo() uc := useCases.NewCreateUserUseCase(userRepo, &stubHasher{}, mailRepo, "https://cms.example.com", discardRecorder) if uc.IsRegistrationOpen() { t.Fatalf("expected IsRegistrationOpen to return false") } _, err := uc.CreateUser(testCtx, useCases.CreateUserCommand{ Username: "bob", Password: "hunter2example12", Email: "bob@example.com", }) if !errors.Is(err, user.ErrRegistrationClosed) { t.Fatalf("expected user.ErrRegistrationClosed, got %v", err) } }) }