- Delete unused files: tempfile subsystem, matcher variants, server/internal - Remove 200+ unused functions across proxy, ssl, lua, http2/3, stream, variable - Fix proxy test type errors (backgroundRefresh ctx→Request) - Move bench/tools mock backend into internal/testutil - Remove corresponding test functions for all deleted code
650 lines
15 KiB
Go
650 lines
15 KiB
Go
// Package security 提供基本认证功能的测试。
|
||
//
|
||
// 该文件测试基本认证模块的各项功能,包括:
|
||
// - 基本认证创建和配置
|
||
// - 用户认证验证
|
||
// - 密码哈希(bcrypt/argon2id)
|
||
// - 用户添加和删除
|
||
// - 凭据提取
|
||
//
|
||
// 作者:xfy
|
||
package security
|
||
|
||
import (
|
||
"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 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 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")
|
||
}
|
||
}
|