P4 #17 手动封禁 Create 非 upsert 导致数据错位: - BanStore 新增 BanIP(ip, reason, duration):事务内清理该 IP 全部 既有记录(兼容历史脏数据)后插入单条封禁记录,计数清零; DisconnectConnection 改用(原裸 Create 为全仓库唯一调用点) - BanEntry.IPAddress 升级 uniqueIndex;InitDB 在 AutoMigrate 前 dedupeBanEntries 清理历史重复行(MySQL 1093 兼容写法) - IncrementFail 原子化:SQL 侧 fail_count+1,miss 时 OnConflict DoNothing 插入兜底并发竞态,回读计数 P5 #18 方案 A(按失败性质区分宽限): - RecordAuthFailure 新增 knownUser 参数:用户名不存在(枚举型 爆破)跳过 3 次宽限、首次触发即封第 1 档;用户名存在(真实 用户输错)保留宽限防误封 - 新增 UserStore.LoginExists(邮箱/裸用户名);五个失败调用点 接线(Web 登录查存在性;SMTP/IMAP/POP3 用登录名;LDAP 侧 存在性不可判定,保守按已知用户处理) - 封禁原因注明「未知用户名,跳过宽限」便于后台审计 新增 8 个测试(-race 通过):BanIP 单行 upsert、唯一索引约束、 16 协程并发计数精确、dedupe 清理/表不存在静默、未知用户即时 封禁、已知用户宽限回归、LoginExists 矩阵。
85 lines
2.2 KiB
Go
85 lines
2.2 KiB
Go
package db
|
|
|
|
// P4 #17 回归:dedupeBanEntries 在 AutoMigrate 前清理历史重复行,
|
|
// 为 ip_address 唯一索引扫清障碍;表不存在时静默跳过。
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// createLegacyBanTable 按旧版结构建表(ip_address 无唯一索引)。
|
|
func createLegacyBanTable(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
gdb, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "test.db")), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
if err := gdb.Exec(`CREATE TABLE ban_entries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ip_address TEXT NOT NULL,
|
|
reason TEXT,
|
|
fail_count INTEGER DEFAULT 0,
|
|
ban_count INTEGER DEFAULT 0,
|
|
expires_at DATETIME,
|
|
created_at DATETIME,
|
|
updated_at DATETIME
|
|
)`).Error; err != nil {
|
|
t.Fatalf("create legacy table: %v", err)
|
|
}
|
|
return gdb
|
|
}
|
|
|
|
func TestDedupeBanEntriesRemovesDuplicates(t *testing.T) {
|
|
gdb := createLegacyBanTable(t)
|
|
|
|
// 同一 IP 三条旧记录(id 1,2,3),另一 IP 一条
|
|
for _, rec := range []struct {
|
|
ip string
|
|
fc int
|
|
}{
|
|
{"1.2.3.4", 1},
|
|
{"1.2.3.4", 2},
|
|
{"1.2.3.4", 3},
|
|
{"5.6.7.8", 4},
|
|
} {
|
|
if err := gdb.Exec(
|
|
"INSERT INTO ban_entries (ip_address, fail_count) VALUES (?, ?)", rec.ip, rec.fc,
|
|
).Error; err != nil {
|
|
t.Fatalf("insert: %v", err)
|
|
}
|
|
}
|
|
|
|
dedupeBanEntries(gdb)
|
|
|
|
// 每 IP 仅剩 id 最大的一条
|
|
var rows []struct {
|
|
IP string `gorm:"column:ip_address"`
|
|
FC int `gorm:"column:fail_count"`
|
|
}
|
|
if err := gdb.Raw("SELECT ip_address, fail_count FROM ban_entries ORDER BY ip_address").Scan(&rows).Error; err != nil {
|
|
t.Fatalf("query: %v", err)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("rows after dedupe = %d, want 2", len(rows))
|
|
}
|
|
if rows[0].IP != "1.2.3.4" || rows[0].FC != 3 {
|
|
t.Fatalf("1.2.3.4 row = %+v, want the max-id row (fail_count=3)", rows[0])
|
|
}
|
|
if rows[1].IP != "5.6.7.8" || rows[1].FC != 4 {
|
|
t.Fatalf("5.6.7.8 row = %+v, want fail_count=4", rows[1])
|
|
}
|
|
}
|
|
|
|
func TestDedupeBanEntriesMissingTableSilent(t *testing.T) {
|
|
gdb, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "test.db")), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
// 表不存在时不应 panic/报错(首次安装场景)
|
|
dedupeBanEntries(gdb)
|
|
}
|