fix: remove the unused user use cases, fix the specs that no longer match the code, include api/middleware and api/pkg/claims in mutation testing, log failures that password reset requests currently ignore, and reject user_id claims that don't fit a user id
This commit is contained in:
1 parent
cbb403011a
commit
3282b0580c
22 files changed
+260
-314
No files matched your search
@@ -2,6 +2,7 @@ package test
|
||||
|
||||
import (
|
||||
"RenewCMS/api/pkg/claims"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -65,3 +66,88 @@ func TestClaimsTokenVersion(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestClaimsUserId(t *testing.T) {
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
|
||||
t.Run("returns the numeric user_id claim", func(t *testing.T) {
|
||||
req := requestWithJwtClaims(t, token, map[string]any{"user_id": uint32(7)})
|
||||
|
||||
got, ok := claims.UserId(req)
|
||||
if !ok {
|
||||
t.Fatalf("expected ok true")
|
||||
}
|
||||
if got != 7 {
|
||||
t.Errorf("expected 7, got %d", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns ok false when the user_id key is missing", func(t *testing.T) {
|
||||
req := requestWithJwtClaims(t, token, map[string]any{"role": "user"})
|
||||
|
||||
_, ok := claims.UserId(req)
|
||||
if ok {
|
||||
t.Errorf("expected ok false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns ok false when user_id is not numeric", func(t *testing.T) {
|
||||
req := requestWithJwtClaims(t, token, map[string]any{"user_id": "one"})
|
||||
|
||||
_, ok := claims.UserId(req)
|
||||
if ok {
|
||||
t.Errorf("expected ok false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns ok false when user_id is negative", func(t *testing.T) {
|
||||
req := requestWithJwtClaims(t, token, map[string]any{"user_id": -1.0})
|
||||
|
||||
_, ok := claims.UserId(req)
|
||||
if ok {
|
||||
t.Errorf("expected ok false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns ok false when user_id has a fractional part", func(t *testing.T) {
|
||||
req := requestWithJwtClaims(t, token, map[string]any{"user_id": 7.5})
|
||||
|
||||
_, ok := claims.UserId(req)
|
||||
if ok {
|
||||
t.Errorf("expected ok false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns ok true when user_id is exactly zero", func(t *testing.T) {
|
||||
req := requestWithJwtClaims(t, token, map[string]any{"user_id": float64(0)})
|
||||
|
||||
got, ok := claims.UserId(req)
|
||||
if !ok {
|
||||
t.Fatalf("expected ok true")
|
||||
}
|
||||
if got != 0 {
|
||||
t.Errorf("expected 0, got %d", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns ok true when user_id is exactly the uint32 max", func(t *testing.T) {
|
||||
req := requestWithJwtClaims(t, token, map[string]any{"user_id": float64(math.MaxUint32)})
|
||||
|
||||
got, ok := claims.UserId(req)
|
||||
if !ok {
|
||||
t.Fatalf("expected ok true")
|
||||
}
|
||||
if got != math.MaxUint32 {
|
||||
t.Errorf("expected %d, got %d", uint32(math.MaxUint32), got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns ok false when user_id is one past the uint32 max", func(t *testing.T) {
|
||||
req := requestWithJwtClaims(t, token, map[string]any{"user_id": float64(4294967296)})
|
||||
|
||||
_, ok := claims.UserId(req)
|
||||
if ok {
|
||||
t.Errorf("expected ok false")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
@@ -77,64 +76,6 @@ func TestCreateUserUseCaseHashesPasswordAndVerificationCode(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateUserUseCaseHashesUpdatedValues(t *testing.T) {
|
||||
cases := []struct {
|
||||
method string
|
||||
plaintext string
|
||||
run func(uc *useCases.UpdateUserUseCase, plaintext string) (user.User, error)
|
||||
stored func(u user.User) string
|
||||
}{
|
||||
{
|
||||
method: "UpdatePassword",
|
||||
plaintext: "newPassw0rd",
|
||||
run: func(uc *useCases.UpdateUserUseCase, plaintext string) (user.User, error) {
|
||||
return uc.UpdatePassword(1, plaintext)
|
||||
},
|
||||
stored: func(u user.User) string { return u.Password },
|
||||
},
|
||||
{
|
||||
method: "UpdatePasswordResetCode",
|
||||
plaintext: "reset-code-123",
|
||||
run: func(uc *useCases.UpdateUserUseCase, plaintext string) (user.User, error) {
|
||||
return uc.UpdatePasswordResetCode(1, plaintext, time.Now().Add(time.Hour))
|
||||
},
|
||||
stored: func(u user.User) string { return u.PasswordResetCode },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" stores the hashed value", func(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
repo.users[1] = user.User{ID: 1}
|
||||
uc := useCases.NewUpdateUserUseCase(repo, &stubHasher{})
|
||||
|
||||
_, err := tc.run(uc, tc.plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
expected := "hashed:" + tc.plaintext
|
||||
if got := tc.stored(repo.users[1]); got != expected {
|
||||
t.Errorf("expected stored value %q, got %q", expected, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(tc.method+" a hashing failure persists nothing", func(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
repo.users[1] = user.User{ID: 1}
|
||||
uc := useCases.NewUpdateUserUseCase(repo, &stubHasher{hashErr: errStubHashFailure})
|
||||
|
||||
_, err := tc.run(uc, tc.plaintext)
|
||||
if !errors.Is(err, errStubHashFailure) {
|
||||
t.Errorf("expected the hashing error, got %v", err)
|
||||
}
|
||||
if got := tc.stored(repo.users[1]); got != "" {
|
||||
t.Errorf("expected the repository update to never be called, got %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticateUserUseCase(t *testing.T) {
|
||||
newRepo := func() *mockUserRepo {
|
||||
repo := newMockUserRepo()
|
||||
|
||||
@@ -172,6 +172,8 @@ func TestRequestPasswordResetHTTP(t *testing.T) {
|
||||
mailRepo.fail.failNext("Send")
|
||||
router := newRequestPasswordResetRouter(userRepo, mailRepo, "https://cms.example.com")
|
||||
|
||||
logBuf := captureRequestPasswordResetLog(t)
|
||||
|
||||
recorder := sendPasswordResetRequest(router, `{"email": "alice@example.com"}`)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
@@ -183,6 +185,12 @@ func TestRequestPasswordResetHTTP(t *testing.T) {
|
||||
if recorder.Header().Get("Set-Cookie") != "" {
|
||||
t.Errorf("expected no jwt cookie to be set, got %s", recorder.Header().Get("Set-Cookie"))
|
||||
}
|
||||
if logBuf.Len() == 0 {
|
||||
t.Fatalf("expected a failure to be written to the process log")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(logBuf.String()), "send") && !strings.Contains(strings.ToLower(logBuf.String()), "mail") {
|
||||
t.Errorf("expected the log to name the mail-send failure, got %q", logBuf.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("An unverified user can request a password reset exactly like a verified one", func(t *testing.T) {
|
||||
|
||||
@@ -3,10 +3,22 @@ package test
|
||||
import (
|
||||
"RenewCMS/internal/domain/user"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func captureRequestPasswordResetLog(t *testing.T) *bytes.Buffer {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
log.SetOutput(&buf)
|
||||
t.Cleanup(func() { log.SetOutput(os.Stderr) })
|
||||
return &buf
|
||||
}
|
||||
|
||||
func newRequestPasswordResetTestUserRepo() *mockUserRepo {
|
||||
repo := newMockUserRepo()
|
||||
repo.users[3] = user.User{ID: 3, Email: "alice@example.com"}
|
||||
@@ -79,6 +91,7 @@ func TestRequestPasswordResetUseCase(t *testing.T) {
|
||||
mailRepo := newMockMailRepo()
|
||||
mailRepo.fail.failNext("Send")
|
||||
uc := useCases.NewRequestPasswordResetUseCase(repo, &stubHasher{}, mailRepo, "https://cms.example.com")
|
||||
logBuf := captureRequestPasswordResetLog(t)
|
||||
|
||||
err := uc.RequestPasswordReset("alice@example.com")
|
||||
if err != nil {
|
||||
@@ -87,12 +100,19 @@ func TestRequestPasswordResetUseCase(t *testing.T) {
|
||||
if repo.updatePasswordResetCodeCalls != 1 {
|
||||
t.Errorf("expected UpdatePasswordResetCode to be called once, got %d", repo.updatePasswordResetCodeCalls)
|
||||
}
|
||||
if logBuf.Len() == 0 {
|
||||
t.Fatalf("expected a failure to be logged")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(logBuf.String()), "send") && !strings.Contains(strings.ToLower(logBuf.String()), "mail") {
|
||||
t.Errorf("expected the log to identify a mail-send failure, got %q", logBuf.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The use case swallows a hash failure and returns nil without storing or sending", func(t *testing.T) {
|
||||
t.Run("The use case logs and swallows a hashing failure", func(t *testing.T) {
|
||||
repo := newRequestPasswordResetTestUserRepo()
|
||||
mailRepo := newMockMailRepo()
|
||||
uc := useCases.NewRequestPasswordResetUseCase(repo, &stubHasher{hashErr: errStubHashFailure}, mailRepo, "https://cms.example.com")
|
||||
logBuf := captureRequestPasswordResetLog(t)
|
||||
|
||||
err := uc.RequestPasswordReset("alice@example.com")
|
||||
if err != nil {
|
||||
@@ -104,13 +124,20 @@ func TestRequestPasswordResetUseCase(t *testing.T) {
|
||||
if len(mailRepo.SentEmails) != 0 {
|
||||
t.Errorf("expected no email to be sent, got %d", len(mailRepo.SentEmails))
|
||||
}
|
||||
if logBuf.Len() == 0 {
|
||||
t.Fatalf("expected a failure to be logged")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(logBuf.String()), "hash") {
|
||||
t.Errorf("expected the log to identify a hashing failure, got %q", logBuf.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The use case swallows a store failure and returns nil without sending", func(t *testing.T) {
|
||||
t.Run("The use case logs and swallows a repository failure while storing the reset code", func(t *testing.T) {
|
||||
repo := newRequestPasswordResetTestUserRepo()
|
||||
repo.fail.failNext("UpdatePasswordResetCode")
|
||||
mailRepo := newMockMailRepo()
|
||||
uc := useCases.NewRequestPasswordResetUseCase(repo, &stubHasher{}, mailRepo, "https://cms.example.com")
|
||||
logBuf := captureRequestPasswordResetLog(t)
|
||||
|
||||
err := uc.RequestPasswordReset("alice@example.com")
|
||||
if err != nil {
|
||||
@@ -119,6 +146,12 @@ func TestRequestPasswordResetUseCase(t *testing.T) {
|
||||
if len(mailRepo.SentEmails) != 0 {
|
||||
t.Errorf("expected no email to be sent, got %d", len(mailRepo.SentEmails))
|
||||
}
|
||||
if logBuf.Len() == 0 {
|
||||
t.Fatalf("expected a failure to be logged")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(logBuf.String()), "updatepasswordresetcode") && !strings.Contains(strings.ToLower(logBuf.String()), "store") {
|
||||
t.Errorf("expected the log to identify a storage failure, got %q", logBuf.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("An unverified user can request a password reset exactly like a verified one", func(t *testing.T) {
|
||||
|
||||
@@ -164,22 +164,4 @@ func TestResetPasswordUseCase(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The generic reset-code update stores an empty code as empty instead of hashing it", func(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
repo.users[3] = user.User{ID: 3, Email: "alice@example.com"}
|
||||
hasher := &stubHasher{}
|
||||
uc := useCases.NewUpdateUserUseCase(repo, hasher)
|
||||
|
||||
_, err := uc.UpdatePasswordResetCode(3, "", time.Time{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if hasher.hashCalls != 0 {
|
||||
t.Errorf("expected Hash to never be called, got %d calls", hasher.hashCalls)
|
||||
}
|
||||
if repo.users[3].PasswordResetCode != "" {
|
||||
t.Errorf("expected stored PasswordResetCode to be empty, got %q", repo.users[3].PasswordResetCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -145,10 +145,8 @@ func newRuntimeSettingsContainer(t *testing.T) *api.UseCases {
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(articleRepo, imageRepo),
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(userRepo),
|
||||
CreateUserUseCase: useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com"),
|
||||
UpdateUserUseCase: useCases.NewUpdateUserUseCase(userRepo, hasher),
|
||||
AuthenticateUserUseCase: useCases.NewAuthenticateUserUseCase(userRepo, hasher),
|
||||
CancelRegistrationUseCase: useCases.NewCancelRegistrationUseCase(userRepo),
|
||||
DeleteUserUseCase: useCases.NewDeleteUserUseCase(userRepo),
|
||||
ListUsersUseCase: useCases.NewListUsersUseCase(userRepo),
|
||||
SendMailUseCase: useCases.NewSendMailUseCase(mailRepo),
|
||||
CreateImageUseCase: useCases.NewCreateImageUseCase(imageRepo),
|
||||
|
||||
@@ -588,101 +588,6 @@ func TestListUsersReturnsEveryUser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUserDelegatesEachOperation(t *testing.T) {
|
||||
newRepo := func() *mockUserRepo {
|
||||
repo := newMockUserRepo()
|
||||
repo.users[1] = user.User{ID: 1}
|
||||
return repo
|
||||
}
|
||||
|
||||
t.Run("UpdateVerificationStatus_id1", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateUserUseCase(repo, &stubHasher{})
|
||||
result, err := uc.UpdateVerificationStatus(1, true)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if result.ID != 1 || !result.IsVerified {
|
||||
t.Errorf("unexpected result: %+v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UpdateVerificationStatus_id9", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateUserUseCase(repo, &stubHasher{})
|
||||
_, err := uc.UpdateVerificationStatus(9, true)
|
||||
if !errors.Is(err, user.ErrUserNotFound) {
|
||||
t.Errorf("expected user.ErrUserNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UpdatePassword_id1", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateUserUseCase(repo, &stubHasher{})
|
||||
result, err := uc.UpdatePassword(1, "newpassword123")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if result.ID != 1 {
|
||||
t.Errorf("expected user id 1, got %d", result.ID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UpdatePassword_id9", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateUserUseCase(repo, &stubHasher{})
|
||||
_, err := uc.UpdatePassword(9, "newpassword123")
|
||||
if !errors.Is(err, user.ErrUserNotFound) {
|
||||
t.Errorf("expected user.ErrUserNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UpdatePasswordResetCode_id1", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateUserUseCase(repo, &stubHasher{})
|
||||
result, err := uc.UpdatePasswordResetCode(1, "reset-code-123", time.Now().Add(time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if result.ID != 1 {
|
||||
t.Errorf("expected user id 1, got %d", result.ID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UpdatePasswordResetCode_id9", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateUserUseCase(repo, &stubHasher{})
|
||||
_, err := uc.UpdatePasswordResetCode(9, "reset-code-123", time.Now().Add(time.Hour))
|
||||
if !errors.Is(err, user.ErrUserNotFound) {
|
||||
t.Errorf("expected user.ErrUserNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteUserReportsMissingUser(t *testing.T) {
|
||||
t.Run("id_1", func(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
repo.users[1] = user.User{ID: 1}
|
||||
uc := useCases.NewDeleteUserUseCase(repo)
|
||||
|
||||
err := uc.DeleteUser(1)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("id_9", func(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
repo.users[1] = user.User{ID: 1}
|
||||
uc := useCases.NewDeleteUserUseCase(repo)
|
||||
|
||||
err := uc.DeleteUser(9)
|
||||
if !errors.Is(err, user.ErrUserNotFound) {
|
||||
t.Errorf("expected user.ErrUserNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSendMailForwardsArgumentsVerbatim(t *testing.T) {
|
||||
cases := []struct {
|
||||
receiver string
|
||||
|
||||
@@ -233,17 +233,3 @@ func TestCreateUser(t *testing.T) {
|
||||
t.Errorf("Verification code was not generated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUserVerification(t *testing.T) {
|
||||
repo := newMockUserRepo()
|
||||
repo.users[1] = user.User{ID: 1, IsVerified: false}
|
||||
uc := useCases.NewUpdateUserUseCase(repo, &stubHasher{})
|
||||
|
||||
res, err := uc.UpdateVerificationStatus(1, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Update failed: %v", err)
|
||||
}
|
||||
if !res.IsVerified {
|
||||
t.Errorf("User should be verified")
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,8 @@ func TestVerifiedAccountRequiredHttp(t *testing.T) {
|
||||
{"a validly signed jwt cookie whose claims carry no user_id key at all", map[string]any{"role": "user"}},
|
||||
{`a validly signed jwt cookie whose user_id claim is the string "one"`, map[string]any{"user_id": "one"}},
|
||||
{"a validly signed jwt cookie whose user_id claim is the number 999, which does not match any user", map[string]any{"user_id": uint32(999)}},
|
||||
{"a validly signed jwt cookie whose user_id claim is the number -1", map[string]any{"user_id": -1.0}},
|
||||
{"a validly signed jwt cookie whose user_id claim is the number 4294967296 (one past the uint32 max)", map[string]any{"user_id": float64(4294967296)}},
|
||||
}
|
||||
for _, tc := range unresolvableTokenCases {
|
||||
t.Run("A token that resolves to no usable account still answers 401, never 403: "+tc.name, func(t *testing.T) {
|
||||
@@ -217,6 +219,28 @@ func TestVerifiedAccountRequiredHttp(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("A fractional user_id claim is never truncated into impersonating a different real user", func(t *testing.T) {
|
||||
articleRepo := newMockArticleRepo()
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.users[7] = domainUser.User{ID: 7, IsVerified: true}
|
||||
router := newVerifiedAccountRequiredRouter(articleRepo, userRepo, newMockImageRepo(), token)
|
||||
|
||||
req := newVerifiedAccountRequiredRequest(http.MethodPost, "/v1/article/", `{"title": "Valid Title", "body": "Some content"}`)
|
||||
req.AddCookie(mintVerifiedAccountRequiredCookie(t, token, map[string]any{"user_id": 7.5}))
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected status 401, got %d, body %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
if rec.Body.String() != apperr.UnauthorizedErrorMessage+"\n" {
|
||||
t.Errorf("unexpected body: %q", rec.Body.String())
|
||||
}
|
||||
if len(articleRepo.articles) != 0 {
|
||||
t.Errorf("expected no article to be created, got %+v", articleRepo.articles)
|
||||
}
|
||||
})
|
||||
|
||||
noCookieCases := []struct {
|
||||
method string
|
||||
path string
|
||||
|
||||
Reference in new issue
Block a user