Files
velox-bot/internal/db/repos/securityrepo/repo_integration_test.go
T
FernandoJVideiraandClaude Sonnet 5.5 132a2cd986 Commands for everything a dashboard can do
- /music skip, pause, resume, stop, repeat, remove, move (DJ role)
- /config view, clear, toggletwitch, streamers, beta-features (server owner)
- /slvl status, remove-reward; /moderation bans, timeouts; /security history
- /beta is now the bot owner's DM-only command: set, off, servers, remove-server, status
- betasettings service and repo methods, with integration tests

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-23 02:10:05 +01:00

256 lines
9.2 KiB
Go

//go:build integration
// Runs against a real Postgres with the dashboard API's migrations applied:
//
// SECURITY_TEST_DB=postgres://... go test -tags integration ./internal/db/repos/securityrepo
package securityrepo
import (
"context"
"database/sql"
"os"
"testing"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
)
func testRepo(t *testing.T) (*Repo, int64) {
t.Helper()
dsn := os.Getenv("SECURITY_TEST_DB")
if dsn == "" {
t.Skip("SECURITY_TEST_DB not set")
}
db, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
const g = 777
for _, tbl := range []string{"security_settings", "security_trusted", "security_quarantines", "security_lockdowns", "security_events", "security_commands"} {
if _, err := db.Exec(`DELETE FROM `+tbl+` WHERE guild_id = $1`, g); err != nil {
t.Fatal(err)
}
}
return NewRepo(db), g
}
func TestSettings(t *testing.T) {
r, g := testRepo(t)
ctx := context.Background()
s, err := r.GetSettings(ctx, g)
if err != nil || s.Configured || s.JoinGateEnabled || s.AntiNukeEnabled || s.UnauthorizedBot != ActionKick || s.AntiNukeLimits[LimitChannelDelete] != 2 || !s.AlertDM {
t.Fatalf("defaults wrong: %+v %v", s, err)
}
for _, f := range []string{FeatureJoinGate, FeatureAntiRaid, FeatureAntiNuke} {
if err := r.SetEnabled(ctx, g, f, true); err != nil {
t.Fatal(err)
}
}
if err := r.SetEnabled(ctx, g, "drop table", true); err == nil {
t.Error("unknown features must be refused, never put into SQL")
}
_ = r.SetAlerts(ctx, g, 10, 20)
if _, err := r.db.Exec(`UPDATE security_settings SET alert_dm = false WHERE guild_id = $1`, g); err != nil {
t.Fatal(err)
}
_ = r.SetQuarantineRole(ctx, g, 30)
_ = r.SetJoinLock(ctx, g, ActionBan)
if _, err := r.db.Exec(`UPDATE security_settings SET joingate_username_patterns = '{nitro,free}', antinuke_limits = '{"ban": 0}' WHERE guild_id = $1`, g); err != nil {
t.Fatal(err)
}
s, err = r.GetSettings(ctx, g)
if err != nil {
t.Fatal(err)
}
if !s.Configured || s.AlertDM || !s.JoinGateEnabled || !s.AntiRaidEnabled || !s.AntiNukeEnabled || s.AlertChannelID != 10 || s.AlertRoleID != 20 || s.QuarantineRoleID != 30 || s.JoinLock != ActionBan {
t.Errorf("saved settings wrong: %+v", s)
}
if len(s.UsernamePatterns) != 2 || s.AntiNukeLimits[LimitBan] != 0 || s.AntiNukeLimits[LimitKick] != 5 {
t.Errorf("patterns or limits wrong (saved limits override defaults key by key): %+v %+v", s.UsernamePatterns, s.AntiNukeLimits)
}
if err := r.SetJoinLock(ctx, g, "everyone"); err == nil {
t.Error("the database must refuse unknown join lock modes")
}
}
func TestTrustedQuarantineLockdown(t *testing.T) {
r, g := testRepo(t)
ctx := context.Background()
_ = r.AddTrusted(ctx, g, Trusted{TargetID: 5, Kind: "user", AddedBy: 1})
_ = r.AddTrusted(ctx, g, Trusted{TargetID: 6, Kind: "role", AddedBy: 1})
if err := r.AddTrusted(ctx, g, Trusted{TargetID: 7, Kind: "everyone", AddedBy: 1}); err == nil {
t.Error("unknown kinds must be refused")
}
list, _ := r.ListTrusted(ctx, g)
if len(list) != 2 {
t.Fatalf("trusted = %+v", list)
}
if ok, _ := r.RemoveTrusted(ctx, g, 5); !ok {
t.Error("remove should report true")
}
_ = r.SaveQuarantine(ctx, Quarantine{GuildID: g, UserID: 9, PreviousRoleIDs: []int64{1, 2}, Reason: "first"})
_ = r.SaveQuarantine(ctx, Quarantine{GuildID: g, UserID: 9, PreviousRoleIDs: nil, Reason: "second"})
q, _ := r.GetQuarantine(ctx, g, 9)
if q == nil || len(q.PreviousRoleIDs) != 2 || q.Reason != "first" {
t.Errorf("the first quarantine record must be kept: %+v", q)
}
qs, _ := r.ListQuarantines(ctx, g)
if len(qs) != 1 {
t.Errorf("list = %+v", qs)
}
_ = r.DeleteQuarantine(ctx, g, 9)
if q, _ := r.GetQuarantine(ctx, g, 9); q != nil {
t.Error("deleted")
}
_ = r.SaveLockdown(ctx, Lockdown{GuildID: g, ChannelID: 50, HadOverwrite: true, PreviousAllow: 1, PreviousDeny: 2, ServerWide: true, LockedBy: 1})
_ = r.SaveLockdown(ctx, Lockdown{GuildID: g, ChannelID: 50, HadOverwrite: false, PreviousAllow: 9, PreviousDeny: 9, LockedBy: 2})
_ = r.SaveLockdown(ctx, Lockdown{GuildID: g, ChannelID: 51, LockedBy: 1})
l, _ := r.GetLockdown(ctx, g, 50)
if l == nil || !l.HadOverwrite || l.PreviousAllow != 1 || !l.ServerWide {
t.Errorf("the first lockdown record must be kept: %+v", l)
}
if all, _ := r.ListLockdowns(ctx, g, false); len(all) != 2 {
t.Errorf("all = %+v", all)
}
if server, _ := r.ListLockdowns(ctx, g, true); len(server) != 1 {
t.Errorf("server-wide = %+v", server)
}
_ = r.DeleteLockdown(ctx, g, 50)
if l, _ := r.GetLockdown(ctx, g, 50); l != nil {
t.Error("deleted")
}
if err := r.AddEvent(ctx, Event{GuildID: g, Kind: EventNuke, ActorID: 3, Detail: "x"}); err != nil {
t.Fatal(err)
}
if _, err := r.db.Exec(`INSERT INTO security_commands (guild_id, command_type, payload, requested_by) VALUES ($1, 'release_quarantine', '{"user_id":"9"}', 1)`, g); err != nil {
t.Fatal(err)
}
if _, err := r.db.Exec(`INSERT INTO security_commands (guild_id, command_type, requested_by) VALUES ($1, 'grant_admin', 1)`, g); err == nil {
t.Error("unknown commands must be refused")
}
cmds, _ := r.ListUnprocessedCommands(ctx)
var mine *Command
for i := range cmds {
if cmds[i].GuildID == g {
mine = &cmds[i]
}
}
if mine == nil || string(mine.Payload) != `{"user_id": "9"}` {
t.Fatalf("command = %+v", mine)
}
_ = r.MarkCommandProcessed(ctx, mine.ID)
}
func TestRaidsSnapshotsAndHeatSettings(t *testing.T) {
r, g := testRepo(t)
ctx := context.Background()
if _, err := r.db.Exec(`DELETE FROM security_raids WHERE guild_id = $1`, g); err != nil {
t.Fatal(err)
}
_, _ = r.db.Exec(`DELETE FROM security_raid_caught WHERE guild_id = $1`, g)
_, _ = r.db.Exec(`DELETE FROM security_snapshots WHERE guild_id = $1`, g)
s, _ := r.GetSettings(ctx, g)
if s.HeatEnabled || s.HeatStrictness != "medium" || s.HeatTimeoutMinutes != 10 || !s.HeatEscalate || s.HeatPanicMembers != 3 {
t.Errorf("heat defaults wrong: %+v", s)
}
_ = r.SetEnabled(ctx, g, FeatureHeat, true)
if _, err := r.db.Exec(`UPDATE security_settings SET heat_blocked_words = '{scam}', antinuke_auto_restore = true WHERE guild_id = $1`, g); err != nil {
t.Fatal(err)
}
s, _ = r.GetSettings(ctx, g)
if !s.HeatEnabled || !s.AntiNukeAutoRestore || len(s.HeatBlockedWords) != 1 {
t.Errorf("heat settings not read back: %+v", s)
}
if _, err := r.db.Exec(`UPDATE security_settings SET heat_strictness = 'insane' WHERE guild_id = $1`, g); err == nil {
t.Error("unknown strictness must be refused by the database")
}
start := time.Now().Add(-time.Minute).UTC().Truncate(time.Millisecond)
_ = r.SaveRaid(ctx, Raid{GuildID: g, StartedAt: start, LastJoinAt: start, InvitesPaused: true, CaughtTotal: 1})
_ = r.SaveRaid(ctx, Raid{GuildID: g, StartedAt: time.Now(), LastJoinAt: start.Add(time.Second), InvitesPaused: true, CaughtTotal: 2})
_ = r.AddRaidCaught(ctx, g, Caught{UserID: 1, Action: "kick", Name: "a"})
_ = r.AddRaidCaught(ctx, g, Caught{UserID: 1, Action: "ban", Name: "a"})
_ = r.AddRaidCaught(ctx, g, Caught{UserID: 2, Action: "kick", Name: "b"})
raids, _ := r.ListRaids(ctx)
var mine *Raid
for i := range raids {
if raids[i].GuildID == g {
mine = &raids[i]
}
}
if mine == nil || !mine.StartedAt.Equal(start) || mine.CaughtTotal != 2 {
t.Fatalf("raid = %+v (the start time is kept on update)", mine)
}
caught, _ := r.ListRaidCaught(ctx, g, 10)
if len(caught) != 2 || caught[0].Action != "kick" {
t.Errorf("caught = %+v (one row per raider)", caught)
}
_ = r.DeleteRaid(ctx, g)
if c, _ := r.ListRaidCaught(ctx, g, 10); len(c) != 0 {
t.Error("deleting a raid clears its list")
}
for i := 0; i < 5; i++ {
if err := r.SaveSnapshot(ctx, Snapshot{GuildID: g, Data: []byte(`{"roles":[],"channels":[]}`), Roles: i}); err != nil {
t.Fatal(err)
}
}
var n int
_ = r.db.QueryRow(`SELECT COUNT(*) FROM security_snapshots WHERE guild_id = $1`, g).Scan(&n)
if n != 3 {
t.Errorf("only the last three snapshots are kept, have %d", n)
}
latest, _ := r.LatestSnapshot(ctx, g)
if latest == nil || latest.Roles != 4 {
t.Errorf("latest = %+v", latest)
}
_ = r.SetEnabled(ctx, g, FeatureAntiNuke, true)
due, _ := r.GuildsDueForSnapshot(ctx, time.Hour)
for _, d := range due {
if d == g {
t.Error("a guild with a fresh snapshot is not due")
}
}
due, _ = r.GuildsDueForSnapshot(ctx, 0)
found := false
for _, d := range due {
found = found || d == g
}
if !found {
t.Error("with a zero max age every anti-nuke guild is due")
}
}
func TestListEvents(t *testing.T) {
r, g := testRepo(t)
ctx := context.Background()
for _, kind := range []string{"settings", "raid_started", "quarantine"} {
if err := r.AddEvent(ctx, Event{GuildID: g, Kind: kind, ActorID: 5, Detail: "d"}); err != nil {
t.Fatal(err)
}
}
if err := r.AddEvent(ctx, Event{GuildID: g + 1, Kind: "other server"}); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _, _ = r.db.Exec(`DELETE FROM security_events WHERE guild_id = $1`, g+1) })
got, err := r.ListEvents(ctx, g, 2)
if err != nil || len(got) != 2 || got[0].Kind != "quarantine" || got[1].Kind != "raid_started" {
t.Fatalf("newest first, limited: %+v %v", got, err)
}
if got[0].CreatedAt.IsZero() || got[0].ActorID != 5 || got[0].TargetID != 0 {
t.Errorf("fields: %+v", got[0])
}
if all, _ := r.ListEvents(ctx, g, 0); len(all) != 3 {
t.Errorf("only this server's events: %v", all)
}
}