package security import ( "context" "errors" "fmt" "strings" "testing" "time" "velox-bot/internal/db/repos/securityrepo" "github.com/bwmarrin/discordgo" ) var ctx = context.Background() // testEngine is a guild (id 100) owned by user 1, with Velox as user 999, // alerts going to channel 500 and pinging role 201. func testEngine(t *testing.T) (*Engine, *fakeDiscord, *fakeStore, *time.Time) { t.Helper() d, st := newDiscord(), newStore() e := New(d, st, "999") e.syncDMs = true now := time.Date(2026, 10, 20, 12, 0, 0, 0, time.UTC) e.now = func() time.Time { return now } s := st.set(100) s.AlertChannelID, s.AlertRoleID = 500, 201 return e, d, st, &now } func refresh(e *Engine) { e.Invalidate(guild) } // snowflake for an account created at t. func snowflakeAt(t time.Time, n int) string { ms := t.UnixMilli() - 1420070400000 return fmt.Sprint(ms<<22 | int64(n)) } func entry(action discordgo.AuditLogAction, user, target string, changes ...*discordgo.AuditLogChange) *discordgo.AuditLogEntry { return &discordgo.AuditLogEntry{ActionType: &action, UserID: user, TargetID: target, Changes: changes} } func change(key discordgo.AuditLogChangeKey, old, new any) *discordgo.AuditLogChange { return &discordgo.AuditLogChange{Key: &key, OldValue: old, NewValue: new} } func contains(list []string, v string) bool { for _, x := range list { if x == v { return true } } return false } // ---- Join Gate ---- func TestCheckJoin(t *testing.T) { now := time.Date(2026, 10, 20, 12, 0, 0, 0, time.UTC) s := securityrepo.Defaults(1) s.JoinGateEnabled = true s.NoAvatar, s.NewAccount, s.AdvertisingName = securityrepo.ActionTimeout, securityrepo.ActionKick, securityrepo.ActionBan s.UsernameFilter, s.UsernamePatterns = securityrepo.ActionKick, []string{" FreeNitro "} s.UnverifiedBot = securityrepo.ActionKick old := snowflakeAt(now.AddDate(-1, 0, 0), 1) young := snowflakeAt(now.Add(-48*time.Hour), 2) for _, c := range []struct { name string u *discordgo.User want string }{ {"clean", &discordgo.User{ID: old, Username: "ana", Avatar: "x"}, "off"}, {"no avatar", &discordgo.User{ID: old, Username: "ana"}, "timeout"}, {"new account beats no avatar", &discordgo.User{ID: young, Username: "ana"}, "kick"}, {"invite in display name", &discordgo.User{ID: old, Username: "ana", GlobalName: "join DISCORD.GG/abc", Avatar: "x"}, "ban"}, {"blocked word, case-insensitive", &discordgo.User{ID: old, Username: "freenitro_giveaway", Avatar: "x"}, "kick"}, {"verified bot passes", &discordgo.User{ID: young, Bot: true, PublicFlags: discordgo.UserFlagVerifiedBot}, "off"}, {"unverified bot", &discordgo.User{ID: young, Bot: true}, "kick"}, } { if got := CheckJoin(s, c.u, now).Action; got != c.want { t.Errorf("%s: got %s, want %s", c.name, got, c.want) } } s.JoinGateEnabled = false if CheckJoin(s, &discordgo.User{ID: young}, now).Action != "off" { t.Error("a disabled Join Gate must not act") } } func TestJoinGateAndJoinLock(t *testing.T) { e, d, st, now := testEngine(t) s := st.set(100) s.JoinGateEnabled, s.NewAccount = true, securityrepo.ActionKick refresh(e) young := d.addMember(snowflakeAt(now.Add(-time.Hour), 5)) if removed := e.OnMemberJoin(ctx, guild, young); !removed || !contains(d.kicked, young.User.ID) { t.Errorf("new account should be kicked: removed=%v kicked=%v", removed, d.kicked) } if d.sentCount() != 1 || !contains(st.eventKinds(), securityrepo.EventJoinGate) { t.Error("the action should be alerted and recorded") } st.set(100).JoinLock = securityrepo.ActionBan refresh(e) m := d.addMember(snowflakeAt(now.AddDate(-2, 0, 0), 6)) if !e.OnMemberJoin(ctx, guild, m) || !contains(d.banned, m.User.ID) { t.Error("a join lock bans everyone new") } } func TestOwnerAndBotAreNeverPunished(t *testing.T) { e, d, st, _ := testEngine(t) st.set(100).JoinLock = securityrepo.ActionBan refresh(e) if e.OnMemberJoin(ctx, guild, d.members["1"]) || len(d.banned) != 0 { t.Error("the owner must never be banned") } if err := e.Quarantine(ctx, guild, "1", "x"); err == nil { t.Error("the owner must never be quarantined") } if err := e.Quarantine(ctx, guild, "999", "x"); err == nil { t.Error("the bot must never quarantine itself") } } // ---- Raids ---- func TestRaidModeStartsActsAndEnds(t *testing.T) { e, d, st, now := testEngine(t) s := st.set(100) s.AntiRaidEnabled, s.RaidJoinCount, s.RaidJoinSeconds, s.RaidAction, s.RaidQuietMinutes = true, 3, 10, securityrepo.ActionKick, 5 refresh(e) old := now.AddDate(-1, 0, 0) join := func(n int) *discordgo.Member { m := d.addMember(snowflakeAt(old, n)) e.OnMemberJoin(ctx, guild, m) return m } a := join(1) *now = now.Add(2 * time.Second) join(2) if len(d.kicked) != 0 || e.RaidActive(guild) { t.Fatal("two joins are not a raid") } *now = now.Add(2 * time.Second) join(3) if !e.RaidActive(guild) || len(d.kicked) != 3 || !contains(d.kicked, a.User.ID) { t.Fatalf("the third join starts a raid and the whole burst is kicked: %v", d.kicked) } if len(d.requests) != 1 || !strings.Contains(d.requests[0], "/incident-actions") || !strings.Contains(d.requests[0], "2026-10-21") { t.Errorf("invites should be paused for 24h: %v", d.requests) } alert := d.channelSends()[0] if alert.Content != "<@&201>" || strings.Join(alert.AllowedMentions.Roles, ",") != "201" || len(alert.AllowedMentions.Users) != 0 { t.Errorf("a raid pings only the alert role: %+v", alert) } // During the raid every join is acted on. *now = now.Add(time.Minute) join(4) if len(d.kicked) != 4 { t.Errorf("joins during a raid are kicked: %v", d.kicked) } // Quiet for 5 minutes: it ends, invites resume, the list is posted. *now = now.Add(6 * time.Minute) e.checkRaids(ctx) if e.RaidActive(guild) { t.Fatal("the raid should end after the quiet period") } if len(d.requests) != 2 || !strings.Contains(d.requests[1], "invites_disabled_until:") { t.Errorf("invites should be resumed: %v", d.requests) } var list string for name, content := range d.files { if strings.HasPrefix(name, "raid-") { list = content } } if strings.Count(list, "\tkick\t") != 4 { t.Errorf("the summary should list the 4 kicked accounts:\n%s", list) } if err := e.EndRaid(ctx, guild, "1"); err == nil { t.Error("ending twice should say there's no raid") } } func TestNoAlertFloodDuringARaid(t *testing.T) { e, d, st, now := testEngine(t) s := st.set(100) s.JoinGateEnabled, s.NoAvatar = true, securityrepo.ActionKick s.AntiRaidEnabled, s.RaidJoinCount, s.RaidAction, s.RaidPauseInvites = true, 3, securityrepo.ActionKick, false refresh(e) for i := 0; i < 50; i++ { e.OnMemberJoin(ctx, guild, &discordgo.Member{User: &discordgo.User{ID: snowflakeAt(now.AddDate(-1, 0, 0), i)}}) // no avatar } if n := d.sentCount(); n > 4 { t.Errorf("a raid of 50 should produce a handful of alerts, got %d", n) } } func TestRaidAlertOnlyTouchesNobody(t *testing.T) { e, d, st, now := testEngine(t) s := st.set(100) s.AntiRaidEnabled, s.RaidJoinCount, s.RaidAction, s.RaidPauseInvites = true, 3, securityrepo.ActionAlert, false refresh(e) for i := 0; i < 5; i++ { e.OnMemberJoin(ctx, guild, d.addMember(snowflakeAt(now.AddDate(-1, 0, 0), i))) } if !e.RaidActive(guild) || len(d.kicked)+len(d.banned) != 0 || len(d.requests) != 0 { t.Error("alert-only raid mode must not remove anyone or pause invites") } } func TestRaidMemoryIsBounded(t *testing.T) { e, d, st, now := testEngine(t) s := st.set(100) s.AntiRaidEnabled, s.RaidJoinCount, s.RaidAction = true, 3, securityrepo.ActionAlert refresh(e) for i := 0; i < maxRecentJoins+300; i++ { e.OnMemberJoin(ctx, guild, &discordgo.Member{User: &discordgo.User{ID: snowflakeAt(now.AddDate(-1, 0, 0), i), Avatar: "a"}}) } r := e.raidFor(guild) if len(r.joins) > maxRecentJoins { t.Errorf("recent joins not capped: %d", len(r.joins)) } _ = d } // ---- Anti-nuke ---- func nukeEngine(t *testing.T) (*Engine, *fakeDiscord, *fakeStore) { e, d, st, _ := testEngine(t) st.set(100).AntiNukeEnabled = true refresh(e) d.addMember("50", "200", "202", "204") // a rogue admin with a booster role return e, d, st } func TestNukeOverTheLimitIsQuarantined(t *testing.T) { e, d, st := nukeEngine(t) // Limit is 2 channel deletes per 10s. e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionChannelDelete, "50", "c1")) e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionChannelDelete, "50", "c2")) if _, ok := st.quarantines[key(100, 50)]; ok { t.Fatal("two deletes are within the limit") } e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionChannelDelete, "50", "c3")) q, ok := st.quarantines[key(100, 50)] if !ok || fmt.Sprint(q.PreviousRoleIDs) != "[200 202]" { t.Fatalf("roles should be saved (not the managed one): %+v", q) } if got := strings.Join(d.members["50"].Roles, ","); got != "204" { t.Errorf("only the unremovable booster role should remain, got %s", got) } if d.timeouts["50"] == nil { t.Error("the nuker should be timed out") } sends := d.channelSends() last := sends[len(sends)-1] if !strings.Contains(last.Embeds[0].Title, "Nuke attempt stopped") || !contains(last.AllowedMentions.Users, "1") { t.Errorf("the owner should be pinged: %+v", last) } // The next delete within a minute doesn't trigger a second response. before := len(d.sent) e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionChannelDelete, "50", "c4")) if len(d.sent) != before { t.Error("one attempt should be answered once") } } func TestTrustedOwnerAndBotAreExempt(t *testing.T) { e, d, st := nukeEngine(t) d.addMember("60", "201") st.trusted[100] = []securityrepo.Trusted{{TargetID: 50, Kind: "user"}, {TargetID: 201, Kind: "role"}} refresh(e) for _, who := range []string{"50", "60", "1", "999"} { for i := 0; i < 5; i++ { e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMemberBanAdd, who, "x")) } } if len(st.quarantines) != 0 { t.Errorf("trusted users, trusted roles, the owner and the bot are exempt: %v", st.quarantines) } } func TestInstantTriggers(t *testing.T) { for name, ent := range map[string]*discordgo.AuditLogEntry{ "prune": entry(discordgo.AuditLogActionMemberPrune, "50", ""), "vanity": entry(discordgo.AuditLogActionGuildUpdate, "50", guild, change("vanity_url_code", "old", "scam")), "perms": entry(discordgo.AuditLogActionRoleUpdate, "50", "202", change("permissions", "0", fmt.Sprint(discordgo.PermissionAdministrator))), "grant": entry(discordgo.AuditLogActionMemberRoleUpdate, "50", "70", change("$add", nil, []any{map[string]any{"id": "200"}})), "newrole": entry(discordgo.AuditLogActionRoleCreate, "50", "x", change("permissions", nil, fmt.Sprint(discordgo.PermissionManageRoles))), "botrole": entry(discordgo.AuditLogActionRoleUpdate, "50", "299", change("name", "Velox", "x")), } { t.Run(name, func(t *testing.T) { e, d, st := nukeEngine(t) d.addMember("70") e.OnAuditLogEntry(ctx, guild, ent) if _, ok := st.quarantines[key(100, 50)]; !ok { t.Errorf("%s should quarantine at once", name) } }) } } func TestHarmlessChangesAreIgnored(t *testing.T) { e, d, st := nukeEngine(t) d.addMember("70") // Renaming a role, removing permissions, giving a plain role. e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionRoleUpdate, "50", "202", change("permissions", fmt.Sprint(discordgo.PermissionAdministrator), "0"))) e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMemberRoleUpdate, "50", "70", change("$add", nil, []any{map[string]any{"id": "202"}}))) e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionChannelUpdate, "50", "c")) if len(st.quarantines) != 0 { t.Error("harmless changes must not trigger anything") } } func TestHarmlessEntriesCostNoLookups(t *testing.T) { e, d, _ := nukeEngine(t) d.lookups = 0 e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMessageDelete, "50", "x")) e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMemberUpdate, "50", "x")) e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMemberRoleUpdate, "50", "70", change("$remove", nil, []any{map[string]any{"id": "202"}}))) e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionGuildUpdate, "50", guild, change("name", "a", "b"))) if d.lookups != 0 { t.Errorf("harmless entries made %d Discord lookups", d.lookups) } } func TestSelfGrantedTrustedRoleIsNotABypass(t *testing.T) { e, d, st := nukeEngine(t) st.trusted[100] = []securityrepo.Trusted{{TargetID: 201, Kind: "role"}} refresh(e) // The rogue admin gives themselves the trusted role... d.members["50"].Roles = append(d.members["50"].Roles, "201") e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMemberRoleUpdate, "50", "50", change("$add", nil, []any{map[string]any{"id": "201"}}))) if _, ok := st.quarantines[key(100, 50)]; !ok { t.Fatal("granting yourself a trusted role must be stopped, not count as trust") } if contains(d.members["50"].Roles, "201") { t.Error("the trusted role should be gone") } } func TestTrustedRoleGrantedToAnAltIsTakenBack(t *testing.T) { e, d, st := nukeEngine(t) st.trusted[100] = []securityrepo.Trusted{{TargetID: 201, Kind: "role"}} refresh(e) d.addMember("71", "201") e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMemberRoleUpdate, "50", "71", change("$add", nil, []any{map[string]any{"id": "201"}}))) if _, ok := st.quarantines[key(100, 50)]; !ok { t.Error("the granter should be quarantined") } if contains(d.members["71"].Roles, "201") { t.Error("the alt should lose the trusted role") } } func TestNukingBotIsKicked(t *testing.T) { e, d, _ := nukeEngine(t) d.members["80"] = &discordgo.Member{User: &discordgo.User{ID: "80", Bot: true}} e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMemberPrune, "80", "")) if !contains(d.kicked, "80") { t.Error("a nuking bot should be kicked") } } func TestAntiNukeOffDoesNothing(t *testing.T) { e, _, st := nukeEngine(t) st.set(100).AntiNukeEnabled = false refresh(e) e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMemberPrune, "50", "")) if len(st.quarantines) != 0 { t.Error("anti-nuke off must not act") } } func TestUnauthorizedBotAdd(t *testing.T) { e, d, st, _ := testEngine(t) st.set(100).JoinGateEnabled = true refresh(e) d.addMember("50", "200") e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionBotAdd, "50", "81")) if !contains(d.kicked, "81") { t.Error("a bot added by someone untrusted should be kicked") } e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionBotAdd, "1", "82")) if contains(d.kicked, "82") { t.Error("the owner may add bots") } } func TestDangerousQuarantineRoleIsNotAssigned(t *testing.T) { e, d, st := nukeEngine(t) st.set(100).QuarantineRoleID = 200 // misconfigured: the Admin role refresh(e) e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMemberPrune, "50", "")) if contains(d.members["50"].Roles, "200") { t.Error("a quarantine role with dangerous permissions must never be assigned") } st.set(100).QuarantineRoleID = 203 refresh(e) d.addMember("51", "201") e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMemberPrune, "51", "")) if !contains(d.members["51"].Roles, "203") { t.Error("a safe quarantine role should be assigned") } } func TestQuarantineRoleVeloxCantAssignDoesNotBlockQuarantine(t *testing.T) { e, d, st := nukeEngine(t) st.set(100).QuarantineRoleID = 203 refresh(e) d.rejectRole = "203" // e.g. moved above Velox's role after it was configured e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMemberPrune, "50", "")) if got := strings.Join(d.members["50"].Roles, ","); got != "204" { t.Errorf("roles must still be removed without the quarantine role, got %s", got) } if d.timeouts["50"] == nil { t.Error("and the member still timed out") } } func TestReleaseGivesRolesBack(t *testing.T) { e, d, st := nukeEngine(t) st.set(100).QuarantineRoleID = 203 refresh(e) e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMemberPrune, "50", "")) // The Mod role was deleted meanwhile... no wait, Member (202) was. d.roles = append(d.roles[:3], d.roles[4:]...) if err := e.Release(ctx, guild, "50", "1"); err != nil { t.Fatal(err) } got := strings.Join(d.members["50"].Roles, ",") if got != "204,200" { t.Errorf("roles after release = %s (want the booster role kept, Admin back, deleted Member skipped, quarantine role gone)", got) } if d.timeouts["50"] != nil { t.Error("the timeout should be removed") } if _, ok := st.quarantines[key(100, 50)]; ok { t.Error("the quarantine record should be gone") } if err := e.Release(ctx, guild, "50", "1"); err == nil { t.Error("releasing twice should fail") } } // ---- Lockdown ---- func TestLockAndUnlockRestoreExactly(t *testing.T) { e, d, _, _ := testEngine(t) d.channels["300"] = &discordgo.Channel{ID: "300", GuildID: guild, Type: discordgo.ChannelTypeGuildText, PermissionOverwrites: []*discordgo.PermissionOverwrite{{ID: guild, Type: discordgo.PermissionOverwriteTypeRole, Allow: discordgo.PermissionAddReactions, Deny: discordgo.PermissionAttachFiles}}} d.channels["301"] = &discordgo.Channel{ID: "301", GuildID: guild, Type: discordgo.ChannelTypeGuildText} d.channels["302"] = &discordgo.Channel{ID: "302", GuildID: guild, Type: discordgo.ChannelTypeGuildCategory} d.channels["400"] = &discordgo.Channel{ID: "400", GuildID: "other", Type: discordgo.ChannelTypeGuildText} if err := e.LockChannel(ctx, guild, "400", "1", false); err == nil { t.Fatal("a channel of another server must never be touched") } locked, failed, err := e.LockServer(ctx, guild, "1") if err != nil || locked != 2 || failed != 0 { t.Fatalf("locked=%d failed=%d err=%v (categories are skipped)", locked, failed, err) } o := d.channels["300"].PermissionOverwrites[0] if o.Deny&discordgo.PermissionSendMessages == 0 || o.Allow&discordgo.PermissionAddReactions != 0 || o.Deny&discordgo.PermissionAttachFiles == 0 { t.Errorf("lock should deny chat and keep other bits: %+v", o) } // Someone changes an unrelated bit during the lockdown; it must survive. o.Allow |= discordgo.PermissionEmbedLinks // Locking again must not overwrite the saved "before" state. _ = e.LockChannel(ctx, guild, "300", "1", false) if _, _, err := e.UnlockServer(ctx, guild, "1"); err != nil { t.Fatal(err) } o = d.channels["300"].PermissionOverwrites[0] if o.Allow != discordgo.PermissionAddReactions|discordgo.PermissionEmbedLinks || o.Deny != discordgo.PermissionAttachFiles { t.Errorf("unlock should restore the original lock bits and keep the new one: allow=%d deny=%d", o.Allow, o.Deny) } if len(d.channels["301"].PermissionOverwrites) != 0 { t.Error("a channel that had no overwrite should have none again") } if _, _, err := e.UnlockServer(ctx, guild, "1"); err == nil { t.Error("unlocking an unlocked server should say so") } } // ---- Configuration rules ---- func TestConfigurationPermissions(t *testing.T) { e, d, st, _ := testEngine(t) admin := d.addMember("50", "200") owner := d.members["1"] trustedUser := d.addMember("60") st.trusted[100] = []securityrepo.Trusted{{TargetID: 60, Kind: "user"}} refresh(e) // Turning on: Manage Server is enough. Turning off: owner or trusted only. if err := e.SetFeature(ctx, guild, admin, discordgo.PermissionManageGuild, securityrepo.FeatureAntiNuke, true); err != nil { t.Errorf("an admin may turn anti-nuke on: %v", err) } if err := e.SetFeature(ctx, guild, admin, discordgo.PermissionAdministrator, securityrepo.FeatureAntiNuke, false); !errors.Is(err, ErrNotTrusted) { t.Errorf("an untrusted admin must not turn anti-nuke off: %v", err) } if err := e.SetFeature(ctx, guild, trustedUser, 0, securityrepo.FeatureAntiNuke, false); err != nil { t.Errorf("a trusted user may: %v", err) } if err := e.SetFeature(ctx, guild, d.addMember("61"), 0, securityrepo.FeatureJoinGate, true); err == nil { t.Error("a regular member must not turn things on") } if err := e.SetFeature(ctx, guild, nil, discordgo.PermissionAdministrator, securityrepo.FeatureJoinGate, true); err == nil { t.Error("no actor, no change") } // Alerts: trusted only, never @everyone, only this server's channels. d.channels["500"] = &discordgo.Channel{ID: "500", GuildID: guild, Type: discordgo.ChannelTypeGuildText} d.channels["501"] = &discordgo.Channel{ID: "501", GuildID: "other", Type: discordgo.ChannelTypeGuildText} if err := e.SetAlerts(ctx, guild, admin, "500", "201"); !errors.Is(err, ErrNotTrusted) { t.Errorf("untrusted alert change: %v", err) } if err := e.SetAlerts(ctx, guild, owner, "500", guild); err == nil { t.Error("alerts must never ping @everyone") } if err := e.SetAlerts(ctx, guild, owner, "501", ""); err == nil { t.Error("another server's channel must be refused") } if err := e.SetAlerts(ctx, guild, owner, "500", "201"); err != nil { t.Error(err) } // Quarantine role: safe roles below Velox only. for role, ok := range map[string]bool{"200": false, guild: false, "204": false, "299": false, "203": true} { err := e.SetQuarantineRole(ctx, guild, owner, role) if (err == nil) != ok { t.Errorf("quarantine role %s: err=%v", role, err) } } // Trust: owner only, never @everyone, bounded. if err := e.Trust(ctx, guild, trustedUser, "user", "50"); !errors.Is(err, ErrNotOwner) { t.Errorf("trusted users must not spread trust: %v", err) } if err := e.Trust(ctx, guild, owner, "role", guild); err == nil { t.Error("trusting @everyone must be refused") } if err := e.Trust(ctx, guild, owner, "user", "404"); err == nil { t.Error("trusting someone not in the server must be refused") } if err := e.Trust(ctx, guild, owner, "role", "201"); err != nil { t.Error(err) } if err := e.Untrust(ctx, guild, admin, "201"); !errors.Is(err, ErrNotOwner) { t.Errorf("only the owner untrusts: %v", err) } } func TestDashboardCommands(t *testing.T) { e, d, st := nukeEngine(t) e.OnAuditLogEntry(ctx, guild, entry(discordgo.AuditLogActionMemberPrune, "50", "")) st.cmds = []securityrepo.Command{ {ID: 1, GuildID: 100, CommandType: "release_quarantine", Payload: []byte(`{"user_id":"50; DROP"}`), RequestedBy: 1}, {ID: 2, GuildID: 100, CommandType: "release_quarantine", Payload: []byte(`{"user_id":"50"}`), RequestedBy: 1}, {ID: 3, GuildID: 100, CommandType: "nope", RequestedBy: 1}, } e.ProcessCommands(ctx) if _, ok := st.quarantines[key(100, 50)]; ok { t.Error("the valid release should have run") } if len(st.done) != 3 { t.Errorf("every command must be marked done: %v", st.done) } _ = d } func TestSafeNameDefusesMentionsAndMarkdown(t *testing.T) { got := safeName(&discordgo.User{Username: "x", GlobalName: "@everyone **hi** <@&1> [link](http://a)"}) if strings.Contains(got, "@everyone") || strings.Contains(got, "<@&") || strings.Contains(got, "**") || strings.Contains(got, "](") { t.Errorf("unsafe name rendering: %q", got) } }