test: add unit tests for auth package
- Test API token generation (uniqueness, format) - Test API token hashing (SHA3-256, deterministic) - Test constant-time token comparison - Test token hash detection - Test password hashing (bcrypt, salted) - Test password verification - Test password complexity validation - Verify bcrypt cost and minimum password length constants
This commit is contained in:
parent
1ccbdfe046
commit
16d70123bf
1 changed files with 318 additions and 0 deletions
318
internal/auth/auth_test.go
Normal file
318
internal/auth/auth_test.go
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateAPIToken(t *testing.T) {
|
||||
token, err := GenerateAPIToken()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateAPIToken() error: %v", err)
|
||||
}
|
||||
|
||||
// Should be 64 hex characters (32 bytes)
|
||||
if len(token) != 64 {
|
||||
t.Errorf("GenerateAPIToken() length = %d, want 64", len(token))
|
||||
}
|
||||
|
||||
// Should be valid hex
|
||||
for _, c := range token {
|
||||
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
|
||||
t.Errorf("GenerateAPIToken() contains invalid hex character: %c", c)
|
||||
}
|
||||
}
|
||||
|
||||
// Should generate unique tokens
|
||||
token2, err := GenerateAPIToken()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateAPIToken() second call error: %v", err)
|
||||
}
|
||||
if token == token2 {
|
||||
t.Error("GenerateAPIToken() generated duplicate tokens")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashAPIToken(t *testing.T) {
|
||||
token := "test-token-12345"
|
||||
hash := HashAPIToken(token)
|
||||
|
||||
// Should be 64 hex characters (SHA3-256)
|
||||
if len(hash) != 64 {
|
||||
t.Errorf("HashAPIToken() length = %d, want 64", len(hash))
|
||||
}
|
||||
|
||||
// Should be deterministic
|
||||
hash2 := HashAPIToken(token)
|
||||
if hash != hash2 {
|
||||
t.Error("HashAPIToken() is not deterministic")
|
||||
}
|
||||
|
||||
// Different tokens should produce different hashes
|
||||
hash3 := HashAPIToken("different-token")
|
||||
if hash == hash3 {
|
||||
t.Error("HashAPIToken() produced same hash for different tokens")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareAPIToken(t *testing.T) {
|
||||
token := "my-secret-token-abc123"
|
||||
hash := HashAPIToken(token)
|
||||
|
||||
// Correct token should match
|
||||
if !CompareAPIToken(token, hash) {
|
||||
t.Error("CompareAPIToken() returned false for correct token")
|
||||
}
|
||||
|
||||
// Wrong token should not match
|
||||
if CompareAPIToken("wrong-token", hash) {
|
||||
t.Error("CompareAPIToken() returned true for wrong token")
|
||||
}
|
||||
|
||||
// Empty token should not match
|
||||
if CompareAPIToken("", hash) {
|
||||
t.Error("CompareAPIToken() returned true for empty token")
|
||||
}
|
||||
|
||||
// Token against wrong hash should not match
|
||||
if CompareAPIToken(token, "0000000000000000000000000000000000000000000000000000000000000000") {
|
||||
t.Error("CompareAPIToken() returned true for wrong hash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareAPIToken_TimingSafe(t *testing.T) {
|
||||
// This test verifies the comparison uses constant-time comparison
|
||||
// by checking that it uses subtle.ConstantTimeCompare internally
|
||||
// (verified by code inspection - this test just ensures the function works)
|
||||
token := "timing-test-token"
|
||||
hash := HashAPIToken(token)
|
||||
|
||||
// Multiple comparisons should all succeed
|
||||
for i := 0; i < 100; i++ {
|
||||
if !CompareAPIToken(token, hash) {
|
||||
t.Errorf("CompareAPIToken() failed on iteration %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAPITokenHashed(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "valid 64-char hex",
|
||||
input: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "actual hash",
|
||||
input: HashAPIToken("test"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "too short",
|
||||
input: "0123456789abcdef",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "too long",
|
||||
input: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef00",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "invalid hex characters",
|
||||
input: "ghijklmnopqrstuv0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "plain token (not hashed)",
|
||||
input: "my-api-token",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "uppercase hex",
|
||||
input: "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := IsAPITokenHashed(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("IsAPITokenHashed(%q) = %v, want %v", tc.input, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashPassword(t *testing.T) {
|
||||
password := "mysecretpassword123"
|
||||
|
||||
hash, err := HashPassword(password)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword() error: %v", err)
|
||||
}
|
||||
|
||||
// Should be a bcrypt hash (starts with $2)
|
||||
if !strings.HasPrefix(hash, "$2") {
|
||||
t.Errorf("HashPassword() did not produce bcrypt hash, got: %s", hash[:10])
|
||||
}
|
||||
|
||||
// Should be 60 characters
|
||||
if len(hash) != 60 {
|
||||
t.Errorf("HashPassword() length = %d, want 60", len(hash))
|
||||
}
|
||||
|
||||
// Same password should produce different hashes (due to salt)
|
||||
hash2, err := HashPassword(password)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword() second call error: %v", err)
|
||||
}
|
||||
if hash == hash2 {
|
||||
t.Error("HashPassword() produced same hash twice (missing salt?)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPasswordHash(t *testing.T) {
|
||||
password := "testpassword456"
|
||||
hash, err := HashPassword(password)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword() error: %v", err)
|
||||
}
|
||||
|
||||
// Correct password should verify
|
||||
if !CheckPasswordHash(password, hash) {
|
||||
t.Error("CheckPasswordHash() returned false for correct password")
|
||||
}
|
||||
|
||||
// Wrong password should not verify
|
||||
if CheckPasswordHash("wrongpassword", hash) {
|
||||
t.Error("CheckPasswordHash() returned true for wrong password")
|
||||
}
|
||||
|
||||
// Empty password should not verify
|
||||
if CheckPasswordHash("", hash) {
|
||||
t.Error("CheckPasswordHash() returned true for empty password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPasswordHashed(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "valid bcrypt hash",
|
||||
input: "$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.qGAL0kcNWQKS5.",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "bcrypt 2b variant",
|
||||
input: "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.qGAL0kcNWQKS5.",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "bcrypt 2y variant",
|
||||
input: "$2y$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.qGAL0kcNWQKS5.",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "plain password",
|
||||
input: "mypassword",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrong length",
|
||||
input: "$2a$12$short",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrong prefix",
|
||||
input: "$1a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.qGAL0kcNWQKS5.",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := IsPasswordHashed(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("IsPasswordHashed(%q) = %v, want %v", tc.input, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePasswordComplexity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "valid long password",
|
||||
password: "thisisaverylongpassword",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "exactly minimum length",
|
||||
password: "123456789012", // 12 chars
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "too short",
|
||||
password: "12345678901", // 11 chars
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "empty password",
|
||||
password: "",
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "single character",
|
||||
password: "a",
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := ValidatePasswordComplexity(tc.password)
|
||||
if tc.wantError && err == nil {
|
||||
t.Errorf("ValidatePasswordComplexity(%q) expected error, got nil", tc.password)
|
||||
}
|
||||
if !tc.wantError && err != nil {
|
||||
t.Errorf("ValidatePasswordComplexity(%q) unexpected error: %v", tc.password, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBcryptCost(t *testing.T) {
|
||||
// Verify bcrypt cost is reasonable (10-14 is typical)
|
||||
if BcryptCost < 10 || BcryptCost > 14 {
|
||||
t.Errorf("BcryptCost = %d, should be between 10 and 14 for reasonable security/performance", BcryptCost)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinPasswordLength(t *testing.T) {
|
||||
// Verify minimum password length is reasonable (at least 8, ideally 12+)
|
||||
if MinPasswordLength < 8 {
|
||||
t.Errorf("MinPasswordLength = %d, should be at least 8", MinPasswordLength)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue