Add fast test hasher to speed up CI tests (#34707)

The production password hasher uses PBKDF2 with 600,000 iterations,
which is slow especially when combined with race detection. This
adds a fast test hasher (work factor 1) that can be used during tests
to speed up user creation.

The fast hasher is only available in non-production builds via build
tags, ensuring it cannot be used in production.
This commit is contained in:
Jesse Hallam 2025-12-11 09:46:21 -04:00 committed by GitHub
parent c519789529
commit 219530c82c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 190 additions and 6 deletions

View file

@ -469,7 +469,7 @@ func TestCheckUserPassword(t *testing.T) {
})
t.Run("successful migration from PBKDF2 with old parameter to new parameter", func(t *testing.T) {
// Create a PBKDF2 hasher with work factor = 10000 instead of the default 60000
// Create a PBKDF2 hasher with work factor = 10000 instead of the default
oldParamPBKDF2, err := hashers.NewPBKDF2(10000, 32)
require.NoError(t, err)
@ -488,8 +488,8 @@ func TestCheckUserPassword(t *testing.T) {
require.Nil(t, appErr)
require.NotEqual(t, pwdBcrypt, updatedUser.Password)
require.Contains(t, updatedUser.Password, "$pbkdf2")
// The new user hash contains the new parameter
require.Contains(t, updatedUser.Password, "w=60000")
// The new user hash should NOT contain the old parameter
require.NotContains(t, updatedUser.Password, "w=10000")
// Re-check with updated password
appErr = th.App.checkUserPassword(user, pwd, false)

View file

@ -135,17 +135,17 @@ func GetHasherFromPHCString(phcString string) (PasswordHasher, phcparser.PHC, er
// Hash hashes the provided password with the latest hashing method.
func Hash(password string) (string, error) {
return latestHasher.Hash(password)
return getLatestHasher().Hash(password)
}
// CompareHashAndPassword compares the parsed [phcparser.PHC] and the provided
// password using the latest hashing method.
func CompareHashAndPassword(phc phcparser.PHC, password string) error {
return latestHasher.CompareHashAndPassword(phc, password)
return getLatestHasher().CompareHashAndPassword(phc, password)
}
// IsLatestHasher verifies that the provided hasher is the latest one. This
// function is useful for identifying stored hashes that require a migration.
func IsLatestHasher(hasher PasswordHasher) bool {
return latestHasher == hasher
return getLatestHasher() == hasher
}

View file

@ -0,0 +1,56 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build !production
package hashers
import "testing"
// testHasher is used during tests to override the latestHasher with a faster
// alternative. This should only be set via SetTestHasher and only in test code.
var testHasher PasswordHasher
// getLatestHasher returns the hasher to use for password operations.
// In non-production builds, if a test hasher has been set via SetTestHasher,
// it will be returned instead of the production latestHasher.
func getLatestHasher() PasswordHasher {
if testHasher != nil {
return testHasher
}
return latestHasher
}
// SetTestHasher sets a hasher to be used instead of the latestHasher during tests.
// This is useful for speeding up tests that create many users, as password hashing
// is computationally expensive. Pass nil to restore normal behavior.
//
// This function is only available in non-production builds and should only be
// called from test code, typically in TestMain. It will panic if called outside
// of a test context.
//
// Example usage:
//
// func TestMain(m *testing.M) {
// hashers.SetTestHasher(hashers.FastTestHasher())
// os.Exit(m.Run())
// }
func SetTestHasher(h PasswordHasher) {
if !testing.Testing() {
panic("SetTestHasher called outside of test context")
}
testHasher = h
}
// FastTestHasher returns a PBKDF2 hasher configured with minimal work factor
// for use in tests while still producing valid password hashes that can be
// verified.
//
// This function is only available in non-production builds.
func FastTestHasher() PasswordHasher {
h, err := NewPBKDF2(1, defaultKeyLength)
if err != nil {
panic("failed to create fast test hasher: " + err.Error())
}
return h
}

View file

@ -0,0 +1,86 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build !production
package hashers
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestSetTestHasher(t *testing.T) {
// Ensure testHasher starts as nil
SetTestHasher(nil)
// Hash should work with nil testHasher (uses latestHasher)
hash1, err := Hash("password")
require.NoError(t, err)
require.NotEmpty(t, hash1)
// Set a fast test hasher
fastHasher := FastTestHasher()
SetTestHasher(fastHasher)
defer SetTestHasher(nil)
// Hash should now use the fast test hasher
hash2, err := Hash("password")
require.NoError(t, err)
require.NotEmpty(t, hash2)
// Verify the hash was generated with different parameters
// The fast hasher uses work factor 1, so the hash should contain w=1
require.Contains(t, hash2, "w=1")
// Verify the password can be verified against the hash
hasher, phc, err := GetHasherFromPHCString(hash2)
require.NoError(t, err)
require.NoError(t, hasher.CompareHashAndPassword(phc, "password"))
require.Error(t, hasher.CompareHashAndPassword(phc, "wrongpassword"))
}
func TestFastTestHasher(t *testing.T) {
hasher := FastTestHasher()
require.NotNil(t, hasher)
// Verify it's a PBKDF2 hasher with work factor 1
pbkdf2Hasher, ok := hasher.(PBKDF2)
require.True(t, ok, "FastTestHasher should return a PBKDF2 hasher")
require.Equal(t, 1, pbkdf2Hasher.workFactor)
// Test that it produces valid hashes
hash, err := hasher.Hash("testpassword")
require.NoError(t, err)
require.Contains(t, hash, "w=1")
// Verify the hash can be validated
parsedHasher, phc, err := GetHasherFromPHCString(hash)
require.NoError(t, err)
require.NoError(t, parsedHasher.CompareHashAndPassword(phc, "testpassword"))
}
func TestGetLatestHasher(t *testing.T) {
// Ensure testHasher starts as nil
SetTestHasher(nil)
// Without test hasher, should return latestHasher
require.Equal(t, latestHasher, getLatestHasher())
// Set a fast test hasher
fastHasher := FastTestHasher()
SetTestHasher(fastHasher)
defer SetTestHasher(nil)
// With test hasher set, should return the test hasher
require.Equal(t, fastHasher, getLatestHasher())
require.NotEqual(t, latestHasher, getLatestHasher())
}
func BenchmarkFastTestHasher(b *testing.B) {
hasher := FastTestHasher()
for b.Loop() {
_, _ = hasher.Hash("password")
}
}

View file

@ -0,0 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build production
package hashers
// getLatestHasher returns the hasher to use for password operations.
// In production builds, this always returns the latestHasher.
func getLatestHasher() PasswordHasher {
return latestHasher
}

View file

@ -136,3 +136,10 @@ func TestIsLatestHasher(t *testing.T) {
require.Equal(t, tc.expectedOutput, actualOutput)
}
}
func BenchmarkDefaultHasher(b *testing.B) {
hasher := DefaultPBKDF2()
for b.Loop() {
_, _ = hasher.Hash("password")
}
}

View file

@ -0,0 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build !production
package testlib
import "github.com/mattermost/mattermost/server/v8/channels/app/password/hashers"
func setupFastTestHasher() {
hashers.SetTestHasher(hashers.FastTestHasher())
}

View file

@ -0,0 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build production
package testlib
func setupFastTestHasher() {}

View file

@ -87,6 +87,9 @@ func NewMainHelperWithOptions(options *HelperOptions) *MainHelper {
log.Fatal(err)
}
// Use a fast password hasher during tests to speed up user creation.
setupFastTestHasher()
if options != nil {
mainHelper.Options = *options