229 lines
8.4 KiB
Go
229 lines
8.4 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/api/handlers/auth"
|
|
validator "RenewCMS/api/pkg/validator"
|
|
domainUser "RenewCMS/internal/domain/user"
|
|
"RenewCMS/internal/infrastructure/hashing"
|
|
"RenewCMS/internal/infrastructure/persistence/models"
|
|
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
|
|
"RenewCMS/internal/infrastructure/useCases"
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/jwtauth/v5"
|
|
)
|
|
|
|
func newMigratedUserRepository(t *testing.T) *persistence.UserRepository {
|
|
t.Helper()
|
|
db := openInMemoryDb(t)
|
|
if err := db.AutoMigrate(&models.User{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
return persistence.NewUserRepository(db)
|
|
}
|
|
|
|
func TestUnverifiedUsersAndReregistrationRepository(t *testing.T) {
|
|
t.Run("Creating two users with no active reset code does not violate the unique constraint", func(t *testing.T) {
|
|
repo := newMigratedUserRepository(t)
|
|
|
|
if _, err := repo.Create(domainUser.User{Username: "alice", Email: "alice@example.com", Password: "hunter2example12", PasswordResetCode: "", VerificationCode: "code-alice"}); err != nil {
|
|
t.Fatalf("expected alice creation to succeed, got %v", err)
|
|
}
|
|
if _, err := repo.Create(domainUser.User{Username: "bob", Email: "bob@example.com", Password: "hunter2example12", PasswordResetCode: "", VerificationCode: "code-bob"}); err != nil {
|
|
t.Fatalf("expected bob creation to succeed, got %v", err)
|
|
}
|
|
|
|
alice, err := repo.GetByUsername("alice")
|
|
if err != nil {
|
|
t.Fatalf("failed to get alice: %v", err)
|
|
}
|
|
if alice.PasswordResetCode != "" {
|
|
t.Errorf("expected empty password reset code for alice, got %q", alice.PasswordResetCode)
|
|
}
|
|
|
|
bob, err := repo.GetByUsername("bob")
|
|
if err != nil {
|
|
t.Fatalf("failed to get bob: %v", err)
|
|
}
|
|
if bob.PasswordResetCode != "" {
|
|
t.Errorf("expected empty password reset code for bob, got %q", bob.PasswordResetCode)
|
|
}
|
|
})
|
|
|
|
t.Run("The unique constraint still rejects two users sharing the same active reset code", func(t *testing.T) {
|
|
repo := newMigratedUserRepository(t)
|
|
|
|
if _, err := repo.Create(domainUser.User{Username: "alice", Email: "alice@example.com", Password: "hunter2example12", PasswordResetCode: "shared-code", VerificationCode: "code-alice"}); err != nil {
|
|
t.Fatalf("expected alice creation to succeed, got %v", err)
|
|
}
|
|
|
|
_, err := repo.Create(domainUser.User{Username: "bob", Email: "bob@example.com", Password: "hunter2example12", PasswordResetCode: "shared-code", VerificationCode: "code-bob"})
|
|
if err == nil {
|
|
t.Fatal("expected Create for bob to fail because of the shared reset code")
|
|
}
|
|
|
|
_, err = repo.GetByUsername("bob")
|
|
if !errors.Is(err, domainUser.ErrUserNotFound) {
|
|
t.Errorf("expected user.ErrUserNotFound, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Deleting a user removes the row so the same username and email can register again", func(t *testing.T) {
|
|
repo := newMigratedUserRepository(t)
|
|
|
|
created, err := repo.Create(domainUser.User{Username: "alice", Email: "alice@example.com", Password: "hunter2example12", VerificationCode: "code-alice"})
|
|
if err != nil {
|
|
t.Fatalf("failed to seed alice: %v", err)
|
|
}
|
|
|
|
if err := repo.Delete(created.ID); err != nil {
|
|
t.Fatalf("expected delete to succeed, got %v", err)
|
|
}
|
|
|
|
recreated, err := repo.Create(domainUser.User{Username: "alice", Email: "alice@example.com", Password: "differentpassword12", VerificationCode: "code-alice-2"})
|
|
if err != nil {
|
|
t.Fatalf("expected alice to be re-registrable after deletion, got %v", err)
|
|
}
|
|
if recreated.ID == created.ID {
|
|
t.Errorf("expected the recreated user to have a different id than %d", created.ID)
|
|
}
|
|
|
|
_, err = repo.Get(created.ID)
|
|
if !errors.Is(err, domainUser.ErrUserNotFound) {
|
|
t.Errorf("expected user.ErrUserNotFound for the deleted id, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Registering the same username while the first account still exists is still rejected", func(t *testing.T) {
|
|
repo := newMigratedUserRepository(t)
|
|
|
|
if _, err := repo.Create(domainUser.User{Username: "alice", Email: "alice@example.com", Password: "hunter2example12", VerificationCode: "code-alice"}); err != nil {
|
|
t.Fatalf("failed to seed alice: %v", err)
|
|
}
|
|
|
|
_, err := repo.Create(domainUser.User{Username: "alice", Email: "someone-else@example.com", Password: "anotherpassword12", VerificationCode: "code-alice-2"})
|
|
if err == nil {
|
|
t.Fatal("expected Create to fail because the username is still in use")
|
|
}
|
|
|
|
users := repo.GetAll()
|
|
count := 0
|
|
for _, u := range users {
|
|
if u.Username == "alice" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("expected exactly one user with username alice, got %d", count)
|
|
}
|
|
})
|
|
|
|
t.Run("Updating a user's reset code to one already active for another user is rejected", func(t *testing.T) {
|
|
repo := newMigratedUserRepository(t)
|
|
|
|
if _, err := repo.Create(domainUser.User{Username: "alice", Email: "alice@example.com", Password: "hunter2example12", PasswordResetCode: "alice-code", VerificationCode: "code-alice"}); err != nil {
|
|
t.Fatalf("expected alice creation to succeed, got %v", err)
|
|
}
|
|
bob, err := repo.Create(domainUser.User{Username: "bob", Email: "bob@example.com", Password: "hunter2example12", VerificationCode: "code-bob"})
|
|
if err != nil {
|
|
t.Fatalf("expected bob creation to succeed, got %v", err)
|
|
}
|
|
|
|
_, err = repo.UpdatePasswordResetCode(bob.ID, "alice-code", time.Now().Add(time.Hour))
|
|
if err == nil {
|
|
t.Fatal("expected UpdatePasswordResetCode for bob to fail because the code is already active for alice")
|
|
}
|
|
|
|
reloadedBob, err := repo.Get(bob.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to reload bob: %v", err)
|
|
}
|
|
if reloadedBob.PasswordResetCode != "" {
|
|
t.Errorf("expected bob's reset code to remain unset, got %q", reloadedBob.PasswordResetCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func newUnverifiedRegistrationRouter(userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
|
hasher := hashing.NewBcryptHasher()
|
|
loginHandler := auth.NewLoginHandler(
|
|
token,
|
|
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com", discardRecorder),
|
|
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
|
true,
|
|
)
|
|
|
|
r := chi.NewRouter()
|
|
r.Route("/v1", func(r chi.Router) {
|
|
r.Route("/auth", func(r chi.Router) {
|
|
r.Post("/register", loginHandler.Register)
|
|
})
|
|
})
|
|
return r
|
|
}
|
|
|
|
func registerViaHttp(t *testing.T, router *chi.Mux, username, password, email string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
body, err := json.Marshal(map[string]string{"username": username, "password": password, "email": email})
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal body: %v", err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/auth/register", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func TestUnverifiedUsersAndReregistrationHttp(t *testing.T) {
|
|
validator.InitValidator()
|
|
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
|
|
|
t.Run("Two unverified users can register one after another without a reset-code collision", func(t *testing.T) {
|
|
repo := newMockUserRepo()
|
|
router := newUnverifiedRegistrationRouter(repo, token)
|
|
|
|
aliceRec := registerViaHttp(t, router, "alice", "hunter2example12", "alice@example.com")
|
|
if aliceRec.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200 for alice, got %d: %s", aliceRec.Code, aliceRec.Body.String())
|
|
}
|
|
if aliceRec.Header().Get("Set-Cookie") == "" {
|
|
t.Errorf("expected a jwt cookie to be set for alice")
|
|
}
|
|
var aliceBody map[string]string
|
|
if err := json.Unmarshal(aliceRec.Body.Bytes(), &aliceBody); err != nil {
|
|
t.Fatalf("failed to decode alice response body: %v", err)
|
|
}
|
|
if aliceBody["message"] != "User registered! HTTPonly jwt cookie created" {
|
|
t.Errorf("unexpected response body for alice: %v", aliceBody)
|
|
}
|
|
|
|
bobRec := registerViaHttp(t, router, "bob", "hunter2example12", "bob@example.com")
|
|
if bobRec.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200 for bob, got %d: %s", bobRec.Code, bobRec.Body.String())
|
|
}
|
|
if bobRec.Header().Get("Set-Cookie") == "" {
|
|
t.Errorf("expected a jwt cookie to be set for bob")
|
|
}
|
|
|
|
if len(repo.users) != 2 {
|
|
t.Fatalf("expected 2 users in the repository, got %d", len(repo.users))
|
|
}
|
|
for _, u := range repo.users {
|
|
if u.IsVerified {
|
|
t.Errorf("expected user %q to be unverified", u.Username)
|
|
}
|
|
if u.PasswordResetCode != "" {
|
|
t.Errorf("expected user %q to have an empty password reset code, got %q", u.Username, u.PasswordResetCode)
|
|
}
|
|
}
|
|
})
|
|
}
|