新增功能: - stream 模块: 流式传输支持,优化大文件和实时数据传输 - Goroutine 池: 限制并发数量,减少调度开销 - 优雅升级: 零停机热升级,继承父进程监听器 - sendfile: 零拷贝文件传输,大文件直接从内核传输 重构改进: - App 结构体封装,支持热升级和信号处理 - 配置结构字段对齐和代码清理 - 完善错误处理和日志记录 Co-Authored-By: Claude <noreply@anthropic.com>
400 lines
8.5 KiB
Go
400 lines
8.5 KiB
Go
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 {
|
|
name string
|
|
cfg *config.AuthConfig
|
|
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 TestBasicAuthProcess(t *testing.T) {
|
|
password := "testpassword"
|
|
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
|
|
auth, err := NewBasicAuth(&config.AuthConfig{
|
|
Type: "basic",
|
|
RequireTLS: false, // Disable TLS for testing
|
|
Users: []config.User{
|
|
{Name: "admin", Password: string(hashedPassword)},
|
|
},
|
|
Realm: "Test Realm",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewBasicAuth() error: %v", err)
|
|
}
|
|
|
|
nextHandler := func(ctx *fasthttp.RequestCtx) {
|
|
ctx.WriteString("OK")
|
|
}
|
|
|
|
handler := auth.Process(nextHandler)
|
|
if handler == nil {
|
|
t.Error("Process() returned nil handler")
|
|
}
|
|
}
|
|
|
|
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 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)
|
|
}
|
|
|
|
// Create a mock request context
|
|
ctx := &fasthttp.RequestCtx{}
|
|
|
|
// Test without Authorization header
|
|
_, _, ok := auth.extractCredentials(ctx)
|
|
if ok {
|
|
t.Error("Expected no credentials without header")
|
|
}
|
|
|
|
// Test with valid Basic auth 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)
|
|
}
|
|
}
|
|
|
|
func TestName(t *testing.T) {
|
|
password := "test"
|
|
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)
|
|
}
|
|
|
|
if auth.Name() != "basic_auth" {
|
|
t.Errorf("Expected name 'basic_auth', got %s", auth.Name())
|
|
}
|
|
}
|