xfy 584ca9170d test(lua,security): 优化测试代码错误返回值处理
- api_socket_tcp_test.go: 使用空白标识符忽略非关键错误返回值
- auth_test.go: 使用空白标识符忽略不需要的 hash 返回值

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 18:24:05 +08:00

751 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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())
}
}