package services import ( "testing" "github.com/stretchr/testify/assert" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" ) // createTestPasswordService creates a minimal PasswordService for testing Hash and Compare func createTestPasswordService() *PasswordService { logger, _ := zap.NewDevelopment() return &PasswordService{ logger: logger, } } func TestPasswordService_Hash(t *testing.T) { service := createTestPasswordService() tests := []struct { name string password string wantErr bool }{ { name: "hash simple password", password: "testpassword123", wantErr: false, }, { name: "hash complex password", password: "SecurePass123!@#", wantErr: false, }, { name: "hash password with special chars", password: "Test@123#Pass$", wantErr: false, }, { name: "hash empty password", password: "", wantErr: false, }, { name: "hash long password", password: "VeryLongPassword123456789!@#$%^&*()", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { hash, err := service.Hash(tt.password) if tt.wantErr { assert.Error(t, err) assert.Empty(t, hash) } else { assert.NoError(t, err) assert.NotEmpty(t, hash) // Verify it's a valid bcrypt hash (starts with $2a$ or $2b$) assert.Contains(t, []string{"$2a$", "$2b$"}, hash[:4]) } }) } } func TestPasswordService_Hash_DifferentResults(t *testing.T) { service := createTestPasswordService() password := "testpassword123" // Hash the same password twice - should produce different hashes (due to salt) hash1, err1 := service.Hash(password) hash2, err2 := service.Hash(password) assert.NoError(t, err1) assert.NoError(t, err2) assert.NotEqual(t, hash1, hash2, "Two hashes of the same password should be different (due to salt)") } func TestPasswordService_Hash_ValidBcryptFormat(t *testing.T) { service := createTestPasswordService() password := "testpassword123" hash, err := service.Hash(password) assert.NoError(t, err) // Verify the hash is valid by trying to parse it cost, err := bcrypt.Cost([]byte(hash)) assert.NoError(t, err) assert.Equal(t, bcryptCost, cost, "Hash should have bcrypt cost 12") } func TestPasswordService_Compare_ValidPassword(t *testing.T) { service := createTestPasswordService() tests := []struct { name string password string }{ { name: "compare valid password", password: "testpassword123", }, { name: "compare valid password with special chars", password: "SecurePass123!@#", }, { name: "compare empty password", password: "", }, { name: "compare long password", password: "VeryLongPassword123456789!@#$%^&*()", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { hash, err := service.Hash(tt.password) assert.NoError(t, err) result := service.Compare(hash, tt.password) assert.True(t, result, "Password should match the hash") }) } } func TestPasswordService_Compare_InvalidPassword(t *testing.T) { service := createTestPasswordService() password := "testpassword123" wrongPassword := "wrongpassword123" hash, err := service.Hash(password) assert.NoError(t, err) result := service.Compare(hash, wrongPassword) assert.False(t, result, "Wrong password should not match the hash") } func TestPasswordService_Compare_EmptyHash(t *testing.T) { service := createTestPasswordService() result := service.Compare("", "testpassword123") assert.False(t, result, "Empty hash should not match any password") } func TestPasswordService_Compare_EmptyPassword(t *testing.T) { service := createTestPasswordService() hash, err := service.Hash("testpassword123") assert.NoError(t, err) result := service.Compare(hash, "") assert.False(t, result, "Empty password should not match the hash") } func TestPasswordService_Compare_InvalidHash(t *testing.T) { service := createTestPasswordService() tests := []struct { name string hash string password string expectedResult bool }{ { name: "invalid hash format", hash: "invalidhash", password: "testpassword123", expectedResult: false, }, { name: "malformed bcrypt hash", hash: "$2a$12$invalid", password: "testpassword123", expectedResult: false, }, { name: "hash with wrong cost", hash: "$2a$10$invalidhashformat", password: "testpassword123", expectedResult: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := service.Compare(tt.hash, tt.password) assert.Equal(t, tt.expectedResult, result) }) } } func TestPasswordService_HashAndCompare_Integration(t *testing.T) { service := createTestPasswordService() testCases := []struct { name string password string }{ { name: "simple password", password: "password123", }, { name: "password with uppercase", password: "Password123", }, { name: "password with special chars", password: "Pass@123!", }, { name: "password with spaces", password: "Pass 123!", }, { name: "password with unicode", password: "Passé123!", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Hash the password hash, err := service.Hash(tc.password) assert.NoError(t, err) assert.NotEmpty(t, hash) // Compare with correct password - should match result := service.Compare(hash, tc.password) assert.True(t, result, "Password should match its hash") // Compare with wrong password - should not match wrongResult := service.Compare(hash, "wrongpassword") assert.False(t, wrongResult, "Wrong password should not match") }) } } func TestPasswordService_Hash_ConsistentCost(t *testing.T) { service := createTestPasswordService() password := "testpassword123" hash, err := service.Hash(password) assert.NoError(t, err) // Verify the cost is 12 cost, err := bcrypt.Cost([]byte(hash)) assert.NoError(t, err) assert.Equal(t, bcryptCost, cost) } func TestPasswordService_Hash_ErrorHandling(t *testing.T) { service := createTestPasswordService() // Test with extremely long password (bcrypt has a limit of 72 bytes) // This should still work as bcrypt truncates, but we test the error path veryLongPassword := make([]byte, 1000) for i := range veryLongPassword { veryLongPassword[i] = 'a' } // This should still succeed as bcrypt handles long passwords hash, err := service.Hash(string(veryLongPassword)) assert.NoError(t, err) assert.NotEmpty(t, hash) // Verify we can still compare it (bcrypt truncates to 72 bytes) result := service.Compare(hash, string(veryLongPassword)) assert.True(t, result, "Long password should still work (truncated by bcrypt)") } func TestPasswordService_Compare_CaseSensitive(t *testing.T) { service := createTestPasswordService() password := "TestPassword123" upperPassword := "TESTPASSWORD123" lowerPassword := "testpassword123" hash, err := service.Hash(password) assert.NoError(t, err) // Exact match should work assert.True(t, service.Compare(hash, password)) // Case variations should not match assert.False(t, service.Compare(hash, upperPassword)) assert.False(t, service.Compare(hash, lowerPassword)) }