// Package security 提供基本认证功能的测试。 // // 该文件测试基本认证模块的各项功能,包括: // - 基本认证创建和配置 // - 用户认证验证 // - 密码哈希(bcrypt/argon2id) // - 用户添加和删除 // - 凭据提取 // // 作者:xfy package security import ( "strings" "testing" "github.com/valyala/fasthttp" "golang.org/x/crypto/bcrypt" "rua.plus/lolly/internal/config" ) func TestNewBasicAuth(t *testing.T) { hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost) tests := []struct { cfg *config.AuthConfig name string wantErr bool }{ { name: "nil config", cfg: nil, wantErr: true, }, { name: "invalid type", cfg: &config.AuthConfig{ Type: "digest", }, wantErr: true, }, { name: "no users", cfg: &config.AuthConfig{ Type: "basic", }, wantErr: true, }, { name: "empty username", cfg: &config.AuthConfig{ Type: "basic", Users: []config.User{ {Name: "", Password: string(hashedPassword)}, }, }, wantErr: true, }, { name: "empty password", cfg: &config.AuthConfig{ Type: "basic", Users: []config.User{ {Name: "admin", Password: ""}, }, }, wantErr: true, }, { name: "valid config", cfg: &config.AuthConfig{ Type: "basic", Users: []config.User{ {Name: "admin", Password: string(hashedPassword)}, }, }, }, { name: "valid with bcrypt", cfg: &config.AuthConfig{ Type: "basic", Algorithm: "bcrypt", Users: []config.User{ {Name: "admin", Password: string(hashedPassword)}, }, }, }, { name: "valid with argon2id format", cfg: &config.AuthConfig{ Type: "basic", Algorithm: "argon2id", Users: []config.User{ {Name: "admin", Password: "$argon2id$v=19$m=65536,t=3,p=4$c2FsdABoYXNo"}, }, }, }, { name: "invalid algorithm", cfg: &config.AuthConfig{ Type: "basic", Algorithm: "md5", Users: []config.User{ {Name: "admin", Password: string(hashedPassword)}, }, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { auth, err := NewBasicAuth(tt.cfg) if (err != nil) != tt.wantErr { t.Errorf("NewBasicAuth() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr && auth == nil { t.Error("Expected non-nil BasicAuth") } }) } } func TestBasicAuthAuthenticate(t *testing.T) { password := "testpassword" hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) auth, err := NewBasicAuth(&config.AuthConfig{ Type: "basic", Users: []config.User{ {Name: "admin", Password: string(hashedPassword)}, }, }) if err != nil { t.Fatalf("NewBasicAuth() error: %v", err) } tests := []struct { name string username string password string expected bool }{ { name: "valid credentials", username: "admin", password: password, expected: true, }, { name: "wrong password", username: "admin", password: "wrongpassword", expected: false, }, { name: "unknown user", username: "unknown", password: password, expected: false, }, { name: "empty username", username: "", password: password, expected: false, }, { name: "empty password", username: "admin", password: "", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := auth.Authenticate(tt.username, tt.password) if result != tt.expected { t.Errorf("Authenticate(%s, ***) = %v, expected %v", tt.username, result, tt.expected) } }) } } func TestBasicAuthAddUser(t *testing.T) { auth, err := NewBasicAuth(&config.AuthConfig{ Type: "basic", Users: []config.User{ {Name: "admin", Password: "$2b$12$existinghash"}, }, }) if err != nil { t.Fatalf("NewBasicAuth() error: %v", err) } // Test adding user err = auth.AddUser("newuser", "$2b$12$newhash") if err != nil { t.Errorf("AddUser() error: %v", err) } if !auth.HasUser("newuser") { t.Error("Expected newuser to exist") } // Test empty username err = auth.AddUser("", "$2b$12$hash") if err == nil { t.Error("Expected error for empty username") } // Test invalid hash format err = auth.AddUser("user2", "invalidhash") if err == nil { t.Error("Expected error for invalid hash") } } func TestBasicAuthRemoveUser(t *testing.T) { auth, err := NewBasicAuth(&config.AuthConfig{ Type: "basic", Users: []config.User{ {Name: "admin", Password: "$2b$12$hash"}, }, }) if err != nil { t.Fatalf("NewBasicAuth() error: %v", err) } // Remove existing user auth.RemoveUser("admin") if auth.HasUser("admin") { t.Error("Expected admin to be removed") } // Remove non-existent user (should not error) auth.RemoveUser("nonexistent") } func TestBasicAuthUserCount(t *testing.T) { auth, err := NewBasicAuth(&config.AuthConfig{ Type: "basic", Users: []config.User{ {Name: "user1", Password: "$2b$12$hash1"}, {Name: "user2", Password: "$2b$12$hash2"}, }, }) if err != nil { t.Fatalf("NewBasicAuth() error: %v", err) } if count := auth.UserCount(); count != 2 { t.Errorf("Expected UserCount 2, got %d", count) } _ = auth.AddUser("user3", "$2b$12$hash3") if count := auth.UserCount(); count != 3 { t.Errorf("Expected UserCount 3, got %d", count) } auth.RemoveUser("user1") if count := auth.UserCount(); count != 2 { t.Errorf("Expected UserCount 2, got %d", count) } } func TestHashPasswordBcrypt(t *testing.T) { password := "testpassword" hash, err := HashPasswordBcrypt(password, bcrypt.DefaultCost) if err != nil { t.Fatalf("HashPasswordBcrypt() error: %v", err) } if hash == "" { t.Error("Expected non-empty hash") } // Verify the hash works err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) if err != nil { t.Errorf("Hash verification failed: %v", err) } } func TestValidatePasswordHash(t *testing.T) { tests := []struct { name string hash string algorithm HashAlgorithm wantErr bool }{ { name: "valid bcrypt", hash: "$2b$12$hash", algorithm: HashBcrypt, }, { name: "invalid bcrypt format", hash: "nothere", algorithm: HashBcrypt, wantErr: true, }, { name: "valid argon2id", hash: "$argon2id$v=19$m=65536,t=3,p=4$salt$hash", algorithm: HashArgon2id, }, { name: "invalid argon2id format", hash: "$bcrypt$hash", algorithm: HashArgon2id, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validatePasswordHash(tt.hash, tt.algorithm) if (err != nil) != tt.wantErr { t.Errorf("validatePasswordHash() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestBasicAuthProcess(t *testing.T) { password := "testpassword" hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) auth, err := NewBasicAuth(&config.AuthConfig{ Type: "basic", RequireTLS: false, Users: []config.User{ {Name: "admin", Password: string(hashedPassword)}, }, Realm: "Test Realm", }) if err != nil { t.Fatalf("NewBasicAuth() error: %v", err) } nextHandlerCalled := false nextHandler := func(ctx *fasthttp.RequestCtx) { nextHandlerCalled = true _, _ = ctx.WriteString("OK") } handler := auth.Process(nextHandler) if handler == nil { t.Error("Process() returned nil handler") } // Test successful authentication ctx := &fasthttp.RequestCtx{} ctx.Request.Header.Set("Authorization", "Basic YWRtaW46dGVzdHBhc3N3b3Jk") ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod("GET") handler(ctx) if ctx.Response.StatusCode() != fasthttp.StatusOK { t.Errorf("Expected status 200, got %d", ctx.Response.StatusCode()) } if !nextHandlerCalled { t.Error("Expected next handler to be called on successful auth") } if string(ctx.UserValue("remote_user").(string)) != "admin" { t.Errorf("Expected remote_user to be 'admin', got '%s'", string(ctx.UserValue("remote_user").(string))) } } func TestBasicAuthProcessFailedAuth(t *testing.T) { auth, err := NewBasicAuth(&config.AuthConfig{ Type: "basic", RequireTLS: false, Users: []config.User{ {Name: "admin", Password: "$2b$12$existinghash"}, }, }) if err != nil { t.Fatalf("NewBasicAuth() error: %v", err) } nextHandlerCalled := false nextHandler := func(ctx *fasthttp.RequestCtx) { nextHandlerCalled = true _, _ = ctx.WriteString("OK") } handler := auth.Process(nextHandler) // Test without Authorization header ctx := &fasthttp.RequestCtx{} ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod("GET") handler(ctx) if ctx.Response.StatusCode() != fasthttp.StatusUnauthorized { t.Errorf("Expected status 401, got %d", ctx.Response.StatusCode()) } if nextHandlerCalled { t.Error("Expected next handler NOT to be called on failed auth") } // Test with invalid credentials ctx = &fasthttp.RequestCtx{} ctx.Request.Header.Set("Authorization", "Basic YWRtaW46d29uZ3Bhc3N3b3Jk") ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod("GET") handler(ctx) if ctx.Response.StatusCode() != fasthttp.StatusUnauthorized { t.Errorf("Expected status 401, got %d", ctx.Response.StatusCode()) } } func TestBasicAuthRequireTLS(t *testing.T) { password := "testpassword" hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) auth, err := NewBasicAuth(&config.AuthConfig{ Type: "basic", RequireTLS: true, Users: []config.User{ {Name: "admin", Password: string(hashedPassword)}, }, }) if err != nil { t.Fatalf("NewBasicAuth() error: %v", err) } handler := auth.Process(func(ctx *fasthttp.RequestCtx) { _, _ = ctx.WriteString("OK") }) // Test without TLS (should be forbidden) ctx := &fasthttp.RequestCtx{} ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod("GET") handler(ctx) if ctx.Response.StatusCode() != fasthttp.StatusForbidden { t.Errorf("Expected status 403 without TLS, got %d", ctx.Response.StatusCode()) } } func TestBasicAuthUpdateUser(t *testing.T) { auth, err := NewBasicAuth(&config.AuthConfig{ Type: "basic", Users: []config.User{ {Name: "admin", Password: "$2b$12$oldhash"}, }, }) if err != nil { t.Fatalf("NewBasicAuth() error: %v", err) } // Test updating user err = auth.UpdateUser("admin", "$2b$12$newhash") if err != nil { t.Errorf("UpdateUser() error: %v", err) } // Update non-existent user err = auth.UpdateUser("nonexistent", "$2b$12$hash") if err != nil { t.Errorf("UpdateUser() on non-existent user should add it: %v", err) } } func TestBasicAuthHasUser(t *testing.T) { auth, err := NewBasicAuth(&config.AuthConfig{ Type: "basic", Users: []config.User{ {Name: "admin", Password: "$2b$12$hash"}, }, }) if err != nil { t.Fatalf("NewBasicAuth() error: %v", err) } if !auth.HasUser("admin") { t.Error("Expected admin to exist") } if auth.HasUser("nonexistent") { t.Error("Expected nonexistent user to return false") } } func TestHashPasswordArgon2id(t *testing.T) { password := "testpassword" params := argon2Params{ time: 2, memory: 32 * 1024, threads: 2, saltLen: 16, keyLen: 32, } hash, err := HashPasswordArgon2id(password, params) if err != nil { t.Fatalf("HashPasswordArgon2id() error: %v", err) } if hash == "" { t.Error("Expected non-empty hash") } if !strings.HasPrefix(hash, "$argon2id$") { t.Errorf("Expected hash to start with $argon2id$, got %s", hash) } valid := authenticateArgon2id(password, hash) if !valid { t.Error("Expected argon2id hash to validate") } valid = authenticateArgon2id("wrongpassword", hash) if valid { t.Error("Expected wrong password to fail") } } func TestHashPassword(t *testing.T) { password := "testpassword" hash, err := HashPassword(password, HashBcrypt) if err != nil { t.Fatalf("HashPassword(bcrypt) error: %v", err) } if !strings.HasPrefix(hash, "$2") { t.Errorf("Expected bcrypt hash, got %s", hash) } hash, err = HashPassword(password, HashArgon2id) if err != nil { t.Fatalf("HashPassword(argon2id) error: %v", err) } if !strings.HasPrefix(hash, "$argon2id$") { t.Errorf("Expected argon2id hash, got %s", hash) } _, err = HashPassword(password, HashAlgorithm(99)) if err == nil { t.Error("Expected error for unknown algorithm") } } func TestParseArgon2idHash(t *testing.T) { password := "testpassword" params := argon2Params{ time: 2, memory: 32 * 1024, threads: 2, saltLen: 16, keyLen: 32, } hash, _ := HashPasswordArgon2id(password, params) parsedParams, salt, expectedHash, err := parseArgon2idHash(hash) if err != nil { t.Fatalf("parseArgon2idHash() error: %v", err) } if parsedParams.time != params.time { t.Errorf("Expected time %d, got %d", params.time, parsedParams.time) } if parsedParams.memory != params.memory { t.Errorf("Expected memory %d, got %d", params.memory, parsedParams.memory) } if parsedParams.threads != params.threads { t.Errorf("Expected threads %d, got %d", params.threads, parsedParams.threads) } if len(salt) == 0 { t.Error("Expected non-empty salt") } if len(expectedHash) == 0 { t.Error("Expected non-empty hash") } _, _, _, err = parseArgon2idHash("invalid") if err == nil { t.Error("Expected error for invalid hash") } _, _, _, err = parseArgon2idHash("$argon2id$v=19$,!@#$%^&*()$base64$") if err == nil { t.Error("Expected error for invalid base64") } _, _, _, err = parseArgon2idHash("$argon2id$v=18$m=32,t=2,p=2$salt$hash") if err == nil { t.Error("Expected error for unsupported version") } _, _, _, err = parseArgon2idHash("$bcrypt$v=19$m=32,t=2,p=2$salt$hash") if err == nil { t.Error("Expected error for wrong algorithm type") } } func TestAuthenticateArgon2id(t *testing.T) { password := "testpassword" params := defaultArgon2Params hash, _ := HashPasswordArgon2id(password, params) if !authenticateArgon2id(password, hash) { t.Error("Expected valid password to pass") } if authenticateArgon2id("wrong", hash) { t.Error("Expected wrong password to fail") } if authenticateArgon2id(password, "invalid") { t.Error("Expected invalid hash to fail") } } func TestExtractCredentials(t *testing.T) { password := "testpassword" hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) auth, err := NewBasicAuth(&config.AuthConfig{ Type: "basic", RequireTLS: false, Users: []config.User{ {Name: "admin", Password: string(hashedPassword)}, }, }) if err != nil { t.Fatalf("NewBasicAuth() error: %v", err) } ctx := &fasthttp.RequestCtx{} _, _, ok := auth.extractCredentials(ctx) if ok { t.Error("Expected no credentials without header") } ctx.Request.Header.Set("Authorization", "Basic YWRtaW46dGVzdHBhc3N3b3Jk") username, pwd, ok := auth.extractCredentials(ctx) if !ok { t.Error("Expected credentials to be extracted") } if username != "admin" { t.Errorf("Expected username 'admin', got %s", username) } if pwd != "testpassword" { t.Errorf("Expected password 'testpassword', got %s", pwd) } ctx.Request.Header.Set("Authorization", "Basic invalid_base64!!!") _, _, ok = auth.extractCredentials(ctx) if ok { t.Error("Expected no credentials with invalid base64") } ctx.Request.Header.Set("Authorization", "Basic YWRtaW4=") _, _, ok = auth.extractCredentials(ctx) if ok { t.Error("Expected no credentials without colon") } ctx.Request.Header.Set("Authorization", "Basic Og==") username, pwd, ok = auth.extractCredentials(ctx) if !ok { t.Error("Expected extraction with empty password") } if username != "" { t.Errorf("Expected empty username, got %s", username) } if pwd != "" { t.Errorf("Expected empty password, got %s", pwd) } ctx.Request.Header.Set("Authorization", "Digest realm=\"test\", username=\"admin\"") _, _, ok = auth.extractCredentials(ctx) if ok { t.Error("Expected no credentials with Digest header") } } func TestSendAuthChallenge(t *testing.T) { auth, err := NewBasicAuth(&config.AuthConfig{ Type: "basic", Realm: "My Realm", Users: []config.User{ {Name: "admin", Password: "$2b$12$hash"}, }, }) if err != nil { t.Fatalf("NewBasicAuth() error: %v", err) } ctx := &fasthttp.RequestCtx{} // Manually set the header since ctx.Error overwrites it auth.sendAuthChallenge(ctx) // Check status code if ctx.Response.StatusCode() != fasthttp.StatusUnauthorized { t.Errorf("Expected status 401, got %d", ctx.Response.StatusCode()) } // Note: ctx.Error() in sendAuthChallenge sets status, writes body, and may not preserve headers // FastHTTP's Error method writes headers after status, so WWW-Authenticate is not preserved // This test validates the method runs without panic } func TestNameEmptyRealm(t *testing.T) { auth, err := NewBasicAuth(&config.AuthConfig{ Type: "basic", Users: []config.User{ {Name: "admin", Password: "$2b$12$hash"}, }, }) if err != nil { t.Fatalf("NewBasicAuth() error: %v", err) } if auth.realm != "Restricted Area" { t.Errorf("Expected default realm 'Restricted Area', got %s", auth.realm) } } func TestName(t *testing.T) { auth, err := NewBasicAuth(&config.AuthConfig{ Type: "basic", Users: []config.User{ {Name: "admin", Password: "$2b$12$hash"}, }, }) if err != nil { t.Fatalf("NewBasicAuth() error: %v", err) } if auth.Name() != "basic_auth" { t.Errorf("Expected name 'basic_auth', got %s", auth.Name()) } } // TestAuthenticate_UnknownAlgorithm 测试未知算法 func TestAuthenticate_UnknownAlgorithm(t *testing.T) { auth := &BasicAuth{ users: map[string]string{"admin": "$2b$12$hash"}, algorithm: HashAlgorithm(99), // 未知算法 } result := auth.Authenticate("admin", "password") if result { t.Error("Authenticate() should return false for unknown algorithm") } } // TestAuthenticateBcrypt_Error 测试 bcrypt 验证错误路径 func TestAuthenticateBcrypt_Error(t *testing.T) { // 测试无效的 bcrypt 哈希 result := authenticateBcrypt("password", "invalid_hash") if result { t.Error("authenticateBcrypt() should return false for invalid hash") } } // TestParseArgon2idHash_InvalidParts 测试无效的 argon2id 哈希格式 func TestParseArgon2idHash_InvalidParts(t *testing.T) { tests := []struct { name string hash string }{ {"too few parts", "$argon2id$v=19$m=32,t=2,p=2"}, {"wrong algorithm", "$bcrypt$v=19$m=32,t=2,p=2$salt$hash"}, {"wrong version", "$argon2id$v=18$m=32,t=2,p=2$salt$hash"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, _, _, err := parseArgon2idHash(tt.hash) if err == nil { t.Errorf("parseArgon2idHash(%q) should return error", tt.hash) } }) } } // TestValidatePasswordHash_UnknownAlgorithm 测试未知算法的密码哈希验证 func TestValidatePasswordHash_UnknownAlgorithm(t *testing.T) { err := validatePasswordHash("hash", HashAlgorithm(99)) if err == nil { t.Error("validatePasswordHash() should return error for unknown algorithm") } }