Files
glchat/internal/permissions/integration_test.go
T

256 lines
8.6 KiB
Go
Raw Normal View History

package permissions_test
import (
"context"
"errors"
"path/filepath"
"testing"
"glchat/internal/database"
"glchat/internal/permissions"
"glchat/internal/source"
"glchat/internal/store"
)
// newTestStore поднимает реальную SQLite со всеми миграциями: так проверяются
// и SQL-запросы, и связка store → permissions (AGENT.md §11.1).
func newTestStore(t *testing.T) *store.Store {
t.Helper()
db, err := database.Open(context.Background(), database.Options{
Path: filepath.Join(t.TempDir(), "glchat.db"),
ReadPool: 2,
Migrate: true,
})
if err != nil {
t.Fatalf("open database: %v", err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Errorf("close database: %v", err)
}
})
return store.New(db)
}
func createUser(t *testing.T, st *store.Store, username string) *store.User {
t.Helper()
user, err := st.CreateUser(context.Background(), store.CreateUserParams{
Username: username,
DisplayName: username,
EmailEnc: "v1:encrypted-" + username,
EmailIndex: "index-" + username,
PasswordHash: "argon2id$test",
})
if err != nil {
t.Fatalf("create user %s: %v", username, err)
}
return user
}
// buildGuild создаёт сервер с ролями по умолчанию: «Администратор» и «Пользователь».
func buildGuild(t *testing.T, st *store.Store, owner *store.User) (*store.Guild, *store.Role, *store.Role) {
t.Helper()
ctx := context.Background()
guild, err := st.CreateGuild(ctx, store.CreateGuildParams{Name: "Test Guild", OwnerID: owner.ID, IsMain: true})
if err != nil {
t.Fatalf("create guild: %v", err)
}
adminRole, err := st.CreateRole(ctx, store.CreateRoleParams{
GuildID: guild.ID, Name: "Администратор", Permissions: uint64(permissions.AllPermissions), Position: 100,
})
if err != nil {
t.Fatalf("create admin role: %v", err)
}
userRole, err := st.CreateRole(ctx, store.CreateRoleParams{
GuildID: guild.ID, Name: "Пользователь", Permissions: uint64(permissions.DefaultUserPermissions),
Position: 0, IsDefault: true, Mentionable: true,
})
if err != nil {
t.Fatalf("create default role: %v", err)
}
if _, err := st.AddGuildMember(ctx, guild.ID, owner.ID, ""); err != nil {
t.Fatalf("add owner as member: %v", err)
}
return guild, adminRole, userRole
}
func TestEngineAgainstRealDatabase(t *testing.T) {
ctx := context.Background()
st := newTestStore(t)
calculator := permissions.NewCalculator(source.New(st))
owner := createUser(t, st, "owner")
member := createUser(t, st, "member")
moderator := createUser(t, st, "moderator")
guild, adminRole, userRole := buildGuild(t, st, owner)
for _, user := range []*store.User{member, moderator} {
if _, err := st.AddGuildMember(ctx, guild.ID, user.ID, ""); err != nil {
t.Fatalf("add member: %v", err)
}
}
moderatorRole, err := st.CreateRole(ctx, store.CreateRoleParams{
GuildID: guild.ID, Name: "Модератор",
Permissions: uint64(permissions.ViewGuild | permissions.ViewChannel | permissions.SendMessages |
permissions.ManageMessages | permissions.KickMembers),
Position: 50,
})
if err != nil {
t.Fatalf("create moderator role: %v", err)
}
if err := st.AssignRole(ctx, guild.ID, moderator.ID, moderatorRole.ID); err != nil {
t.Fatalf("assign moderator role: %v", err)
}
channel, err := st.CreateChannel(ctx, store.CreateChannelParams{
GuildID: &guild.ID, Type: store.ChannelText, Name: "общий",
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
private, err := st.CreateChannel(ctx, store.CreateChannelParams{
GuildID: &guild.ID, Type: store.ChannelText, Name: "приватный",
})
if err != nil {
t.Fatalf("create private channel: %v", err)
}
// Приватный канал: @user не видит, модератор — видит.
if err := st.SetChannelOverride(ctx, store.ChannelOverride{
ChannelID: private.ID, TargetType: "role", TargetID: userRole.ID,
Deny: uint64(permissions.ViewChannel | permissions.ReadMessageHistory),
}); err != nil {
t.Fatalf("set override: %v", err)
}
if err := st.SetChannelOverride(ctx, store.ChannelOverride{
ChannelID: private.ID, TargetType: "role", TargetID: moderatorRole.ID,
Allow: uint64(permissions.ViewChannel | permissions.ReadMessageHistory),
}); err != nil {
t.Fatalf("set moderator override: %v", err)
}
calculator.InvalidateAll()
t.Run("member sees public channel only", func(t *testing.T) {
public, err := calculator.Channel(ctx, guild.ID, channel.ID, member.ID, false)
if err != nil {
t.Fatalf("channel permissions: %v", err)
}
if !public.Can(permissions.SendMessages) {
t.Fatal("member must send messages in the public channel")
}
hidden, err := calculator.Channel(ctx, guild.ID, private.ID, member.ID, false)
if err != nil {
t.Fatalf("private channel permissions: %v", err)
}
if hidden.CanViewChannel() {
t.Fatal("member must not see the private channel")
}
if err := permissions.RequireChannel(hidden, permissions.ViewChannel); !errors.Is(err, permissions.ErrDenied) {
t.Fatalf("RequireChannel = %v, want ErrDenied", err)
}
})
t.Run("moderator sees private channel and can moderate", func(t *testing.T) {
visible, err := calculator.Channel(ctx, guild.ID, private.ID, moderator.ID, false)
if err != nil {
t.Fatalf("channel permissions: %v", err)
}
if !visible.CanViewChannel() || !visible.Can(permissions.ReadMessageHistory) {
t.Fatal("moderator must see the private channel")
}
if !visible.Has(permissions.KickMembers) {
t.Fatal("moderator must keep guild-level KICK_MEMBERS")
}
if visible.Can(permissions.BanMembers) {
t.Fatal("moderator must not receive BAN_MEMBERS")
}
})
t.Run("owner has administrator everywhere", func(t *testing.T) {
resolved, err := calculator.Channel(ctx, guild.ID, private.ID, owner.ID, false)
if err != nil {
t.Fatalf("channel permissions: %v", err)
}
if !resolved.IsOwner || !resolved.Can(permissions.Administrator) {
t.Fatal("owner must have ADMINISTRATOR")
}
if !resolved.CanViewChannel() {
t.Fatal("owner must see any channel")
}
})
t.Run("instance admin bypasses privacy", func(t *testing.T) {
stranger := createUser(t, st, "instance-admin")
resolved, err := calculator.Channel(ctx, guild.ID, private.ID, stranger.ID, true)
if err != nil {
t.Fatalf("channel permissions: %v", err)
}
if !resolved.CanViewChannel() || !resolved.Can(permissions.ManageRoles) {
t.Fatal("instance admin must bypass privacy and permissions")
}
})
t.Run("timeout blocks writing", func(t *testing.T) {
timeout := store.TimeoutFromNow(1)
if err := st.UpdateGuildMember(ctx, guild.ID, member.ID, nil, &timeout); err != nil {
t.Fatalf("set timeout: %v", err)
}
calculator.InvalidateAll()
resolved, err := calculator.Channel(ctx, guild.ID, channel.ID, member.ID, false)
if err != nil {
t.Fatalf("channel permissions: %v", err)
}
if !resolved.TimedOut {
t.Fatal("timeout must be reflected in resolved permissions")
}
if resolved.Can(permissions.SendMessages) {
t.Fatal("timed out member must not write")
}
if !resolved.Can(permissions.ViewChannel) {
t.Fatal("timed out member must still read")
}
})
t.Run("administrator role ignores overrides", func(t *testing.T) {
if err := st.AssignRole(ctx, guild.ID, member.ID, adminRole.ID); err != nil {
t.Fatalf("assign admin role: %v", err)
}
calculator.InvalidateAll()
resolved, err := calculator.Channel(ctx, guild.ID, private.ID, member.ID, false)
if err != nil {
t.Fatalf("channel permissions: %v", err)
}
if !resolved.CanViewChannel() {
t.Fatal("administrator role must ignore deny overrides")
}
})
}
func TestRoleHierarchyHelpersWithRealRoles(t *testing.T) {
ctx := context.Background()
st := newTestStore(t)
owner := createUser(t, st, "owner2")
guild, adminRole, userRole := buildGuild(t, st, owner)
moderatorRole, err := st.CreateRole(ctx, store.CreateRoleParams{
GuildID: guild.ID, Name: "Модератор", Position: 50,
Permissions: uint64(permissions.KickMembers),
})
if err != nil {
t.Fatalf("create role: %v", err)
}
toData := func(role *store.Role) permissions.RoleData {
return permissions.RoleData{ID: role.ID, Permissions: permissions.Permission(role.Permissions), Position: role.Position}
}
if !permissions.OutranksRole([]permissions.RoleData{toData(moderatorRole)}, toData(userRole), false) {
t.Fatal("moderator must outrank the default role")
}
if permissions.OutranksRole([]permissions.RoleData{toData(moderatorRole)}, toData(adminRole), false) {
t.Fatal("moderator must not outrank the administrator role")
}
if !permissions.OutranksRole(nil, toData(adminRole), true) {
t.Fatal("owner must outrank every role")
}
}