2026-03-15 07:07:59 +01:00
|
|
|
package permissions_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"testing"
|
|
|
|
|
|
2026-08-28 06:54:32 +02:00
|
|
|
"github.com/J3vb/OwnCord/Server/permissions"
|
2026-03-15 07:07:59 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ─── Constant value tests ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
// TestPermissionBitValues verifies every constant matches the SCHEMA.md bitfield.
|
|
|
|
|
func TestPermissionBitValues(t *testing.T) {
|
|
|
|
|
cases := []struct {
|
|
|
|
|
name string
|
|
|
|
|
got int64
|
|
|
|
|
expected int64
|
|
|
|
|
}{
|
|
|
|
|
{"SendMessages", permissions.SendMessages, 0x0001},
|
|
|
|
|
{"ReadMessages", permissions.ReadMessages, 0x0002},
|
|
|
|
|
{"AttachFiles", permissions.AttachFiles, 0x0020},
|
|
|
|
|
{"AddReactions", permissions.AddReactions, 0x0040},
|
|
|
|
|
{"ConnectVoice", permissions.ConnectVoice, 0x0200},
|
|
|
|
|
{"SpeakVoice", permissions.SpeakVoice, 0x0400},
|
|
|
|
|
{"UseVideo", permissions.UseVideo, 0x0800},
|
|
|
|
|
{"ShareScreen", permissions.ShareScreen, 0x1000},
|
|
|
|
|
{"ManageMessages", permissions.ManageMessages, 0x10000},
|
|
|
|
|
{"ManageChannels", permissions.ManageChannels, 0x20000},
|
|
|
|
|
{"KickMembers", permissions.KickMembers, 0x40000},
|
|
|
|
|
{"BanMembers", permissions.BanMembers, 0x80000},
|
|
|
|
|
{"MuteMembers", permissions.MuteMembers, 0x100000},
|
|
|
|
|
{"ManageRoles", permissions.ManageRoles, 0x1000000},
|
|
|
|
|
{"ManageServer", permissions.ManageServer, 0x2000000},
|
|
|
|
|
{"ManageInvites", permissions.ManageInvites, 0x4000000},
|
|
|
|
|
{"ViewAuditLog", permissions.ViewAuditLog, 0x8000000},
|
|
|
|
|
{"Administrator", permissions.Administrator, 0x40000000},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tc := range cases {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
if tc.got != tc.expected {
|
|
|
|
|
t.Errorf("%s: got 0x%X, want 0x%X", tc.name, tc.got, tc.expected)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestRoleIDConstants verifies the predefined role IDs match SCHEMA.md defaults.
|
|
|
|
|
func TestRoleIDConstants(t *testing.T) {
|
|
|
|
|
cases := []struct {
|
|
|
|
|
name string
|
|
|
|
|
got int64
|
|
|
|
|
expected int64
|
|
|
|
|
}{
|
|
|
|
|
{"OwnerRoleID", permissions.OwnerRoleID, 1},
|
|
|
|
|
{"AdminRoleID", permissions.AdminRoleID, 2},
|
|
|
|
|
{"ModeratorRoleID", permissions.ModeratorRoleID, 3},
|
|
|
|
|
{"MemberRoleID", permissions.MemberRoleID, 4},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tc := range cases {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
if tc.got != tc.expected {
|
|
|
|
|
t.Errorf("%s: got %d, want %d", tc.name, tc.got, tc.expected)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestOwnerRolePosition verifies the owner position sentinel value.
|
|
|
|
|
func TestOwnerRolePosition(t *testing.T) {
|
|
|
|
|
if permissions.OwnerRolePosition != 100 {
|
|
|
|
|
t.Errorf("OwnerRolePosition: got %d, want 100", permissions.OwnerRolePosition)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── HasPerm tests ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestHasPerm_MatchingBitReturnsTrue(t *testing.T) {
|
|
|
|
|
rolePerms := permissions.SendMessages | permissions.ReadMessages | permissions.ConnectVoice
|
|
|
|
|
if !permissions.HasPerm(rolePerms, permissions.SendMessages) {
|
|
|
|
|
t.Error("expected HasPerm to return true when bit is set")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHasPerm_MissingBitReturnsFalse(t *testing.T) {
|
|
|
|
|
rolePerms := permissions.ReadMessages | permissions.ConnectVoice
|
|
|
|
|
if permissions.HasPerm(rolePerms, permissions.SendMessages) {
|
|
|
|
|
t.Error("expected HasPerm to return false when bit is not set")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHasPerm_ZeroPermsReturnsFalse(t *testing.T) {
|
|
|
|
|
if permissions.HasPerm(0, permissions.SendMessages) {
|
|
|
|
|
t.Error("expected HasPerm(0, ...) to return false")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHasPerm_ZeroRequiredReturnsFalse(t *testing.T) {
|
|
|
|
|
// Requiring perm 0 should never match — 0 is not a valid permission bit.
|
|
|
|
|
if permissions.HasPerm(permissions.Administrator, 0) {
|
|
|
|
|
t.Error("expected HasPerm(..., 0) to return false for zero required perm")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHasPerm_MultipleBitsSetOnlyChecksRequired(t *testing.T) {
|
|
|
|
|
// rolePerms has many bits; we ask about one that is present.
|
|
|
|
|
rolePerms := permissions.SendMessages | permissions.ManageMessages | permissions.BanMembers
|
|
|
|
|
if !permissions.HasPerm(rolePerms, permissions.ManageMessages) {
|
|
|
|
|
t.Error("expected HasPerm to find ManageMessages in combined bitfield")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHasPerm_AllBitsSet(t *testing.T) {
|
|
|
|
|
// 0x7FFFFFFF (Owner default) must satisfy every individual permission.
|
|
|
|
|
allPerms := int64(0x7FFFFFFF)
|
|
|
|
|
perms := []int64{
|
|
|
|
|
permissions.SendMessages, permissions.ReadMessages, permissions.AttachFiles,
|
2026-04-02 17:00:27 +02:00
|
|
|
permissions.AddReactions, permissions.ConnectVoice,
|
2026-03-15 07:07:59 +01:00
|
|
|
permissions.SpeakVoice, permissions.UseVideo, permissions.ShareScreen,
|
|
|
|
|
permissions.ManageMessages, permissions.ManageChannels, permissions.KickMembers,
|
|
|
|
|
permissions.BanMembers, permissions.MuteMembers, permissions.ManageRoles,
|
|
|
|
|
permissions.ManageServer, permissions.ManageInvites, permissions.ViewAuditLog,
|
|
|
|
|
permissions.Administrator,
|
|
|
|
|
}
|
|
|
|
|
for _, p := range perms {
|
|
|
|
|
if !permissions.HasPerm(allPerms, p) {
|
|
|
|
|
t.Errorf("expected all-bits owner to have perm 0x%X", p)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── HasAdmin tests ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestHasAdmin_AdministratorBitSet(t *testing.T) {
|
|
|
|
|
if !permissions.HasAdmin(permissions.Administrator) {
|
|
|
|
|
t.Error("expected HasAdmin to return true when Administrator bit is set")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHasAdmin_AdministratorBitWithOthers(t *testing.T) {
|
|
|
|
|
combined := permissions.SendMessages | permissions.Administrator | permissions.BanMembers
|
|
|
|
|
if !permissions.HasAdmin(combined) {
|
|
|
|
|
t.Error("expected HasAdmin to return true with Administrator bit among others")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHasAdmin_NoAdministratorBit(t *testing.T) {
|
|
|
|
|
if permissions.HasAdmin(permissions.SendMessages | permissions.BanMembers) {
|
|
|
|
|
t.Error("expected HasAdmin to return false without Administrator bit")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHasAdmin_ZeroPerms(t *testing.T) {
|
|
|
|
|
if permissions.HasAdmin(0) {
|
|
|
|
|
t.Error("expected HasAdmin(0) to return false")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHasAdmin_AdminRolePermsMissingBit(t *testing.T) {
|
|
|
|
|
// Admin role default is 0x3FFFFFFF — bit 30 (Administrator) is NOT set.
|
|
|
|
|
adminDefault := int64(0x3FFFFFFF)
|
|
|
|
|
if permissions.HasAdmin(adminDefault) {
|
|
|
|
|
t.Error("expected HasAdmin to return false for Admin role (0x3FFFFFFF lacks bit 30)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHasAdmin_OwnerRolePermsHasBit(t *testing.T) {
|
|
|
|
|
// Owner role default is 0x7FFFFFFF — bit 30 IS set.
|
|
|
|
|
ownerDefault := int64(0x7FFFFFFF)
|
|
|
|
|
if !permissions.HasAdmin(ownerDefault) {
|
|
|
|
|
t.Error("expected HasAdmin to return true for Owner role (0x7FFFFFFF has bit 30)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 14:45:34 +02:00
|
|
|
// ─── HasServerPerm tests ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
// TestHasServerPerm locks the contract api.RequirePermission inherits: admin
|
|
|
|
|
// bypass, and ALL-of semantics for multi-bit masks (a raw `perms&mask != 0`
|
|
|
|
|
// test would make them any-of).
|
|
|
|
|
func TestHasServerPerm(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
rolePerms int64
|
|
|
|
|
perm int64
|
|
|
|
|
want bool
|
|
|
|
|
}{
|
|
|
|
|
{"admin bypasses a bit it lacks", permissions.Administrator, permissions.ManageInvites, true},
|
|
|
|
|
// Deliberately UNLIKE HasPerm(Administrator, 0) == false: the admin
|
|
|
|
|
// bypass short-circuits before the zero-mask guard.
|
|
|
|
|
{"admin with zero mask", permissions.Administrator, 0, true},
|
|
|
|
|
{"exact bit held", permissions.ManageInvites, permissions.ManageInvites, true},
|
|
|
|
|
{"bit not held", permissions.SendMessages, permissions.ManageInvites, false},
|
|
|
|
|
{"non-admin with zero mask", permissions.SendMessages, 0, false},
|
|
|
|
|
{"multi-bit mask partially held", permissions.SendMessages, permissions.SendMessages | permissions.ManageRoles, false},
|
|
|
|
|
{"multi-bit mask fully held", permissions.SendMessages | permissions.ManageRoles, permissions.SendMessages | permissions.ManageRoles, true},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
if got := permissions.HasServerPerm(tt.rolePerms, tt.perm); got != tt.want {
|
|
|
|
|
t.Errorf("HasServerPerm() = %v, want %v", got, tt.want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 07:07:59 +01:00
|
|
|
// ─── EffectivePerms tests ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
// EffectivePerms(rolePerm, allow, deny) = (rolePerm & ^deny) | allow
|
|
|
|
|
|
|
|
|
|
func TestEffectivePerms_NoOverrides(t *testing.T) {
|
|
|
|
|
base := permissions.SendMessages | permissions.ReadMessages
|
|
|
|
|
got := permissions.EffectivePerms(base, 0, 0)
|
|
|
|
|
if got != base {
|
|
|
|
|
t.Errorf("EffectivePerms with no overrides: got 0x%X, want 0x%X", got, base)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEffectivePerms_AllowAddsPermission(t *testing.T) {
|
|
|
|
|
base := permissions.ReadMessages
|
|
|
|
|
allow := permissions.SendMessages
|
|
|
|
|
got := permissions.EffectivePerms(base, allow, 0)
|
|
|
|
|
want := permissions.ReadMessages | permissions.SendMessages
|
|
|
|
|
if got != want {
|
|
|
|
|
t.Errorf("EffectivePerms allow: got 0x%X, want 0x%X", got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEffectivePerms_DenyRemovesPermission(t *testing.T) {
|
|
|
|
|
base := permissions.SendMessages | permissions.ReadMessages | permissions.ConnectVoice
|
|
|
|
|
deny := permissions.ConnectVoice
|
|
|
|
|
got := permissions.EffectivePerms(base, 0, deny)
|
|
|
|
|
want := permissions.SendMessages | permissions.ReadMessages
|
|
|
|
|
if got != want {
|
|
|
|
|
t.Errorf("EffectivePerms deny: got 0x%X, want 0x%X", got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEffectivePerms_AllowAndDenyTogether(t *testing.T) {
|
|
|
|
|
// deny removes ConnectVoice; allow grants ManageMessages.
|
|
|
|
|
base := permissions.SendMessages | permissions.ReadMessages | permissions.ConnectVoice
|
|
|
|
|
allow := permissions.ManageMessages
|
|
|
|
|
deny := permissions.ConnectVoice
|
|
|
|
|
got := permissions.EffectivePerms(base, allow, deny)
|
|
|
|
|
want := permissions.SendMessages | permissions.ReadMessages | permissions.ManageMessages
|
|
|
|
|
if got != want {
|
|
|
|
|
t.Errorf("EffectivePerms allow+deny: got 0x%X, want 0x%X", got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEffectivePerms_AllowOverridesDeny(t *testing.T) {
|
|
|
|
|
// When both allow and deny target the same bit, allow wins
|
|
|
|
|
// because the formula applies deny first, then allow.
|
|
|
|
|
base := permissions.SendMessages
|
|
|
|
|
allow := permissions.ConnectVoice
|
|
|
|
|
deny := permissions.ConnectVoice
|
|
|
|
|
got := permissions.EffectivePerms(base, allow, deny)
|
|
|
|
|
// deny strips ConnectVoice, then allow adds it back.
|
|
|
|
|
want := permissions.SendMessages | permissions.ConnectVoice
|
|
|
|
|
if got != want {
|
|
|
|
|
t.Errorf("EffectivePerms allow overrides deny: got 0x%X, want 0x%X", got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEffectivePerms_ZeroBase(t *testing.T) {
|
|
|
|
|
allow := permissions.SendMessages | permissions.ReadMessages
|
|
|
|
|
got := permissions.EffectivePerms(0, allow, 0)
|
|
|
|
|
if got != allow {
|
|
|
|
|
t.Errorf("EffectivePerms zero base: got 0x%X, want 0x%X", got, allow)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEffectivePerms_ZeroAll(t *testing.T) {
|
|
|
|
|
got := permissions.EffectivePerms(0, 0, 0)
|
|
|
|
|
if got != 0 {
|
|
|
|
|
t.Errorf("EffectivePerms all zero: got 0x%X, want 0", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEffectivePerms_DenyAllGrantNone(t *testing.T) {
|
|
|
|
|
base := int64(0x7FFFFFFF)
|
|
|
|
|
deny := int64(0x7FFFFFFF)
|
|
|
|
|
got := permissions.EffectivePerms(base, 0, deny)
|
|
|
|
|
if got != 0 {
|
|
|
|
|
t.Errorf("EffectivePerms deny all: got 0x%X, want 0", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-01 08:41:09 +02:00
|
|
|
|
|
|
|
|
// ─── HasPerm — combined multi-bit checks ────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestHasPerm_RequiresAllBitsPresent(t *testing.T) {
|
|
|
|
|
// Require both SendMessages AND ManageMessages; user only has SendMessages.
|
|
|
|
|
rolePerms := permissions.SendMessages | permissions.ReadMessages
|
|
|
|
|
combined := permissions.SendMessages | permissions.ManageMessages
|
|
|
|
|
|
|
|
|
|
if permissions.HasPerm(rolePerms, combined) {
|
|
|
|
|
t.Error("should fail when only some of the required bits are present")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHasPerm_CombinedBitsAllPresent(t *testing.T) {
|
|
|
|
|
rolePerms := permissions.SendMessages | permissions.ReadMessages | permissions.ManageMessages
|
|
|
|
|
combined := permissions.SendMessages | permissions.ManageMessages
|
|
|
|
|
|
|
|
|
|
if !permissions.HasPerm(rolePerms, combined) {
|
|
|
|
|
t.Error("should succeed when all required combined bits are present")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── EffectivePerms — channel override edge cases ───────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestEffectivePerms_DenyAllThenAllowOne(t *testing.T) {
|
|
|
|
|
base := permissions.SendMessages | permissions.ReadMessages | permissions.ConnectVoice
|
2026-04-01 11:38:11 +02:00
|
|
|
deny := int64(0x7FFFFFFF) // deny everything
|
|
|
|
|
allow := permissions.ReadMessages // re-allow just ReadMessages
|
2026-04-01 08:41:09 +02:00
|
|
|
|
|
|
|
|
eff := permissions.EffectivePerms(base, allow, deny)
|
|
|
|
|
if eff != permissions.ReadMessages {
|
|
|
|
|
t.Errorf("deny-all + allow-one: got 0x%X, want 0x%X", eff, permissions.ReadMessages)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEffectivePerms_MultipleDenyMultipleAllow(t *testing.T) {
|
|
|
|
|
base := permissions.SendMessages | permissions.ReadMessages | permissions.AttachFiles | permissions.ConnectVoice
|
|
|
|
|
deny := permissions.SendMessages | permissions.ConnectVoice
|
|
|
|
|
allow := permissions.ManageChannels | permissions.ManageMessages
|
|
|
|
|
|
|
|
|
|
eff := permissions.EffectivePerms(base, allow, deny)
|
|
|
|
|
|
|
|
|
|
// Should keep: ReadMessages, AttachFiles (not denied)
|
|
|
|
|
// Should lose: SendMessages, ConnectVoice (denied)
|
|
|
|
|
// Should gain: ManageChannels, ManageMessages (allowed)
|
|
|
|
|
want := permissions.ReadMessages | permissions.AttachFiles | permissions.ManageChannels | permissions.ManageMessages
|
|
|
|
|
if eff != want {
|
|
|
|
|
t.Errorf("multi deny+allow: got 0x%X, want 0x%X", eff, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Role hierarchy simulation ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestRoleHierarchy_OwnerHasMorePermsThanAdmin(t *testing.T) {
|
2026-04-01 11:38:11 +02:00
|
|
|
ownerPerms := int64(0x7FFFFFFF) // Owner default
|
|
|
|
|
adminPerms := int64(0x3FFFFFFF) // Admin default (no Administrator bit)
|
2026-04-01 08:41:09 +02:00
|
|
|
|
|
|
|
|
if !permissions.HasAdmin(ownerPerms) {
|
|
|
|
|
t.Error("owner should be admin")
|
|
|
|
|
}
|
|
|
|
|
if permissions.HasAdmin(adminPerms) {
|
|
|
|
|
t.Error("admin role should NOT have Administrator bit")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Owner can ManageServer via admin bypass.
|
|
|
|
|
// Admin can ManageServer via direct bit.
|
|
|
|
|
if !permissions.HasPerm(adminPerms, permissions.ManageServer) {
|
|
|
|
|
t.Error("admin should have ManageServer bit directly")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRoleHierarchy_MemberLacksModPerms(t *testing.T) {
|
|
|
|
|
memberPerms := int64(1635) // Default member permissions from schema
|
|
|
|
|
|
|
|
|
|
modPerms := []int64{
|
|
|
|
|
permissions.ManageMessages,
|
|
|
|
|
permissions.ManageChannels,
|
|
|
|
|
permissions.KickMembers,
|
|
|
|
|
permissions.BanMembers,
|
|
|
|
|
permissions.ManageRoles,
|
|
|
|
|
permissions.ManageServer,
|
|
|
|
|
permissions.Administrator,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, p := range modPerms {
|
|
|
|
|
if permissions.HasPerm(memberPerms, p) {
|
|
|
|
|
t.Errorf("member (0x%X) should not have permission 0x%X", memberPerms, p)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRoleHierarchy_MemberHasBasicPerms(t *testing.T) {
|
2026-04-01 11:38:11 +02:00
|
|
|
memberPerms := int64(1635) //nolint:gocritic // documenting the bitmask composition, not commented-out code
|
2026-04-01 08:41:09 +02:00
|
|
|
|
|
|
|
|
basicPerms := []struct {
|
|
|
|
|
name string
|
|
|
|
|
perm int64
|
|
|
|
|
}{
|
|
|
|
|
{"SendMessages", permissions.SendMessages},
|
|
|
|
|
{"ReadMessages", permissions.ReadMessages},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tc := range basicPerms {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
if !permissions.HasPerm(memberPerms, tc.perm) {
|
|
|
|
|
t.Errorf("member should have %s", tc.name)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Permission bits are unique powers of 2 ─────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestPermissionBits_AreDistinctPowersOfTwo(t *testing.T) {
|
|
|
|
|
bits := []int64{
|
|
|
|
|
permissions.SendMessages, permissions.ReadMessages, permissions.AttachFiles,
|
2026-04-02 17:00:27 +02:00
|
|
|
permissions.AddReactions, permissions.ConnectVoice,
|
2026-04-01 08:41:09 +02:00
|
|
|
permissions.SpeakVoice, permissions.UseVideo, permissions.ShareScreen,
|
|
|
|
|
permissions.ManageMessages, permissions.ManageChannels, permissions.KickMembers,
|
|
|
|
|
permissions.BanMembers, permissions.MuteMembers, permissions.ManageRoles,
|
|
|
|
|
permissions.ManageServer, permissions.ManageInvites, permissions.ViewAuditLog,
|
|
|
|
|
permissions.Administrator,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seen := make(map[int64]bool)
|
|
|
|
|
for _, b := range bits {
|
|
|
|
|
if b&(b-1) != 0 {
|
|
|
|
|
t.Errorf("permission 0x%X is not a power of 2", b)
|
|
|
|
|
}
|
|
|
|
|
if seen[b] {
|
|
|
|
|
t.Errorf("duplicate permission bit: 0x%X", b)
|
|
|
|
|
}
|
|
|
|
|
seen[b] = true
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-01 22:06:14 +02:00
|
|
|
|
|
|
|
|
// ─── HasAnyPerm / AdminPerimeter / Name tests ────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestHasAnyPerm_AnyOfSemantics(t *testing.T) {
|
|
|
|
|
rolePerms := permissions.KickMembers | permissions.SendMessages
|
|
|
|
|
// One matching bit is enough, unlike HasPerm's ALL-of.
|
|
|
|
|
if !permissions.HasAnyPerm(rolePerms, permissions.KickMembers|permissions.ManageServer) {
|
|
|
|
|
t.Error("expected HasAnyPerm to match on KickMembers")
|
|
|
|
|
}
|
|
|
|
|
if permissions.HasPerm(rolePerms, permissions.KickMembers|permissions.ManageServer) {
|
|
|
|
|
t.Error("HasPerm must stay ALL-of")
|
|
|
|
|
}
|
|
|
|
|
if permissions.HasAnyPerm(rolePerms, permissions.ManageServer|permissions.ViewAuditLog) {
|
|
|
|
|
t.Error("expected HasAnyPerm to reject a disjoint mask")
|
|
|
|
|
}
|
|
|
|
|
if permissions.HasAnyPerm(rolePerms, 0) {
|
|
|
|
|
t.Error("a zero mask must never be satisfied")
|
|
|
|
|
}
|
|
|
|
|
if permissions.HasAnyPerm(0, permissions.AdminPerimeter) {
|
|
|
|
|
t.Error("a zero role must never satisfy the perimeter")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAdminPerimeter_Membership(t *testing.T) {
|
|
|
|
|
admitted := []int64{
|
|
|
|
|
permissions.Administrator, permissions.ManageChannels, permissions.ManageRoles,
|
|
|
|
|
permissions.ManageServer, permissions.ViewAuditLog, permissions.KickMembers,
|
|
|
|
|
permissions.BanMembers, permissions.MuteMembers,
|
|
|
|
|
}
|
|
|
|
|
for _, p := range admitted {
|
|
|
|
|
if !permissions.HasAnyPerm(p, permissions.AdminPerimeter) {
|
|
|
|
|
t.Errorf("%s should admit to the admin perimeter", permissions.Name(p))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Bits with no admin-panel route must not open the perimeter.
|
|
|
|
|
refused := []int64{
|
|
|
|
|
permissions.SendMessages, permissions.ReadMessages, permissions.ManageMessages,
|
|
|
|
|
permissions.ManageInvites, permissions.ConnectVoice,
|
|
|
|
|
}
|
|
|
|
|
for _, p := range refused {
|
|
|
|
|
if permissions.HasAnyPerm(p, permissions.AdminPerimeter) {
|
|
|
|
|
t.Errorf("%s must not admit to the admin perimeter", permissions.Name(p))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// The seeded Moderator role (migration 001) gets in.
|
|
|
|
|
if !permissions.HasAnyPerm(0x000FFFFF, permissions.AdminPerimeter) {
|
|
|
|
|
t.Error("the seeded Moderator mask should admit to the admin perimeter")
|
|
|
|
|
}
|
|
|
|
|
// The seeded Member role does not.
|
|
|
|
|
if permissions.HasAnyPerm(1635, permissions.AdminPerimeter) {
|
|
|
|
|
t.Error("the seeded Member mask must not admit to the admin perimeter")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestName_KnownAndUnknownBits(t *testing.T) {
|
|
|
|
|
if got := permissions.Name(permissions.ManageChannels); got != "MANAGE_CHANNELS" {
|
|
|
|
|
t.Errorf("Name(ManageChannels) = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := permissions.Name(permissions.ViewAuditLog); got != "VIEW_AUDIT_LOG" {
|
|
|
|
|
t.Errorf("Name(ViewAuditLog) = %q", got)
|
|
|
|
|
}
|
|
|
|
|
// Zero, multi-bit and undefined values have no single name.
|
|
|
|
|
for _, bit := range []int64{0, permissions.KickMembers | permissions.BanMembers, 0x4} {
|
|
|
|
|
if got := permissions.Name(bit); got != "UNKNOWN" {
|
|
|
|
|
t.Errorf("Name(0x%X) = %q, want UNKNOWN", bit, got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestMentionEveryone_BitIsFreeAndNamed locks phase 3's new bit: 21 was
|
|
|
|
|
// unassigned, it is part of AllPerms so an externally supplied mask keeps it,
|
|
|
|
|
// and it stays out of the admin perimeter (mentioning is not moderation).
|
|
|
|
|
func TestMentionEveryone_BitIsFreeAndNamed(t *testing.T) {
|
|
|
|
|
if permissions.MentionEveryone != 0x200000 {
|
|
|
|
|
t.Errorf("MentionEveryone = 0x%X, want 0x200000 (bit 21)", permissions.MentionEveryone)
|
|
|
|
|
}
|
|
|
|
|
for _, other := range []int64{
|
|
|
|
|
permissions.SendMessages, permissions.ReadMessages, permissions.AttachFiles,
|
|
|
|
|
permissions.AddReactions, permissions.ConnectVoice, permissions.SpeakVoice,
|
|
|
|
|
permissions.UseVideo, permissions.ShareScreen, permissions.ManageMessages,
|
|
|
|
|
permissions.ManageChannels, permissions.KickMembers, permissions.BanMembers,
|
|
|
|
|
permissions.MuteMembers, permissions.ManageRoles, permissions.ManageServer,
|
|
|
|
|
permissions.ManageInvites, permissions.ViewAuditLog, permissions.Administrator,
|
|
|
|
|
} {
|
|
|
|
|
if other&permissions.MentionEveryone != 0 {
|
|
|
|
|
t.Errorf("bit 21 collides with 0x%X", other)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if permissions.AllPerms&permissions.MentionEveryone == 0 {
|
|
|
|
|
t.Error("AllPerms must include MentionEveryone")
|
|
|
|
|
}
|
|
|
|
|
if permissions.AdminPerimeter&permissions.MentionEveryone != 0 {
|
|
|
|
|
t.Error("MentionEveryone must not admit to the admin perimeter")
|
|
|
|
|
}
|
|
|
|
|
if got := permissions.Name(permissions.MentionEveryone); got != "MENTION_EVERYONE" {
|
|
|
|
|
t.Errorf("Name(MentionEveryone) = %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|