c0537d3c17
- internal/bootstrap: создание инстанс-админа, главного сервера, ролей «Администратор» и «Пользователь» (all permissions / дефолтный набор §6.3), общей текстовой комнаты; повторный запуск идемпотентен - админ получает флаг is_instance_admin и системный бейдж instance_admin - CLI: glchat bootstrap-admin, reset-password, make-admin, remove-admin (работают внутри контейнера через DATA_DIR) - auth: UserByEmail, SetPassword (отзывает сессии), RegisterAdmin (в обход флага регистрации — bootstrap выполняет установщик) - тесты: создание админа и главного сервера, идемпотентность, автовступление нового пользователя с ролью @user
191 lines
5.4 KiB
Go
191 lines
5.4 KiB
Go
package bootstrap_test
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"glchat/internal/auth"
|
|
"glchat/internal/bootstrap"
|
|
"glchat/internal/config"
|
|
"glchat/internal/database"
|
|
"glchat/internal/permissions"
|
|
"glchat/internal/store"
|
|
)
|
|
|
|
func newRunner(t *testing.T) (*bootstrap.Runner, *store.Store, *auth.Service) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
db, err := database.Open(ctx, 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() { _ = db.Close() })
|
|
|
|
st := store.New(db)
|
|
logger := slog.New(slog.DiscardHandler)
|
|
cfg := config.Config{
|
|
SessionPepper: "bootstrap-pepper",
|
|
MasterKey: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
|
|
Argon2MemoryKiB: 1024,
|
|
Argon2Iterations: 1,
|
|
Argon2Parallelism: 1,
|
|
}
|
|
authService, err := auth.New(ctx, cfg, st, logger)
|
|
if err != nil {
|
|
t.Fatalf("auth.New: %v", err)
|
|
}
|
|
return bootstrap.New(st, authService, logger), st, authService
|
|
}
|
|
|
|
func TestBootstrapCreatesAdminAndMainGuild(t *testing.T) {
|
|
ctx := context.Background()
|
|
runner, st, _ := newRunner(t)
|
|
|
|
result, err := runner.Run(ctx, bootstrap.Options{
|
|
Email: "owner@example.com",
|
|
Password: "correct-horse-battery",
|
|
MainGuild: "Главный сервер",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
if !result.CreatedUser || !result.CreatedGuild {
|
|
t.Fatalf("unexpected result: %+v", result)
|
|
}
|
|
|
|
admin, err := st.GetUser(ctx, result.UserID)
|
|
if err != nil {
|
|
t.Fatalf("GetUser: %v", err)
|
|
}
|
|
if !admin.IsInstanceAdmin {
|
|
t.Fatal("bootstrap admin must have the instance admin flag")
|
|
}
|
|
if len(admin.Badges) == 0 || admin.Badges[0] != "instance_admin" {
|
|
t.Fatalf("admin badge is missing: %+v", admin.Badges)
|
|
}
|
|
|
|
settings, err := st.InstanceSettings(ctx)
|
|
if err != nil {
|
|
t.Fatalf("InstanceSettings: %v", err)
|
|
}
|
|
if settings.MainGuildID != result.GuildID {
|
|
t.Fatalf("main guild id = %d, want %d", settings.MainGuildID, result.GuildID)
|
|
}
|
|
|
|
roles, err := st.ListGuildRoles(ctx, result.GuildID)
|
|
if err != nil {
|
|
t.Fatalf("ListGuildRoles: %v", err)
|
|
}
|
|
if len(roles) != 2 {
|
|
t.Fatalf("main guild must have 2 default roles, got %d", len(roles))
|
|
}
|
|
var defaultRole *store.Role
|
|
for i := range roles {
|
|
if roles[i].IsDefault {
|
|
defaultRole = &roles[i]
|
|
}
|
|
}
|
|
if defaultRole == nil {
|
|
t.Fatal("default role is missing")
|
|
}
|
|
if permissions.Permission(defaultRole.Permissions) != permissions.DefaultUserPermissions {
|
|
t.Fatalf("default role permissions = %s", permissions.Permission(defaultRole.Permissions))
|
|
}
|
|
|
|
// Владелец — участник с ролью администратора.
|
|
memberRoles, err := st.MemberRoles(ctx, result.GuildID, result.UserID)
|
|
if err != nil {
|
|
t.Fatalf("MemberRoles: %v", err)
|
|
}
|
|
found := false
|
|
for _, role := range memberRoles {
|
|
if role.ID == result.AdminRoleID {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("owner must receive the administrator role")
|
|
}
|
|
|
|
// Общая текстовая комната создана.
|
|
channels, err := st.ListGuildChannels(ctx, result.GuildID)
|
|
if err != nil {
|
|
t.Fatalf("ListGuildChannels: %v", err)
|
|
}
|
|
if len(channels) != 1 || channels[0].Name != "общий" {
|
|
t.Fatalf("unexpected channels: %+v", channels)
|
|
}
|
|
}
|
|
|
|
func TestBootstrapIsIdempotent(t *testing.T) {
|
|
ctx := context.Background()
|
|
runner, st, authService := newRunner(t)
|
|
options := bootstrap.Options{Email: "owner@example.com", Password: "correct-horse-battery"}
|
|
|
|
first, err := runner.Run(ctx, options)
|
|
if err != nil {
|
|
t.Fatalf("first Run: %v", err)
|
|
}
|
|
second, err := runner.Run(ctx, options)
|
|
if err != nil {
|
|
t.Fatalf("second Run: %v", err)
|
|
}
|
|
if second.GuildID != first.GuildID {
|
|
t.Fatalf("second run created another guild: %d != %d", second.GuildID, first.GuildID)
|
|
}
|
|
if second.CreatedUser || second.CreatedGuild {
|
|
t.Fatalf("second run must not create anything: %+v", second)
|
|
}
|
|
guilds, err := st.ListAllGuilds(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListAllGuilds: %v", err)
|
|
}
|
|
if len(guilds) != 1 {
|
|
t.Fatalf("guild count = %d, want 1", len(guilds))
|
|
}
|
|
|
|
// Новый пользователь может войти с заданным паролем.
|
|
if _, _, _, err := authService.Login(ctx, auth.LoginInput{
|
|
Email: "owner@example.com", Password: "correct-horse-battery",
|
|
}); err == nil {
|
|
t.Fatal("instance admin without 2FA must not be able to log in")
|
|
}
|
|
}
|
|
|
|
func TestNewUserJoinsMainGuild(t *testing.T) {
|
|
ctx := context.Background()
|
|
runner, st, authService := newRunner(t)
|
|
result, err := runner.Run(ctx, bootstrap.Options{Email: "owner@example.com", Password: "correct-horse-battery"})
|
|
if err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
|
|
user, _, _, err := authService.Register(ctx, auth.RegisterInput{
|
|
Username: "newcomer", Email: "newcomer@example.com", Password: "correct-horse-battery",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Register: %v", err)
|
|
}
|
|
guilds, err := st.ListGuildsForUser(ctx, user.ID)
|
|
if err != nil {
|
|
t.Fatalf("ListGuildsForUser: %v", err)
|
|
}
|
|
if len(guilds) != 1 || guilds[0].ID != result.GuildID {
|
|
t.Fatalf("newcomer must join the main guild: %+v", guilds)
|
|
}
|
|
roles, err := st.MemberRoles(ctx, result.GuildID, user.ID)
|
|
if err != nil {
|
|
t.Fatalf("MemberRoles: %v", err)
|
|
}
|
|
if len(roles) != 1 || !roles[0].IsDefault {
|
|
t.Fatalf("newcomer must get the default role: %+v", roles)
|
|
}
|
|
if !permissions.Permission(roles[0].Permissions).Has(permissions.ViewGuild) {
|
|
t.Fatal("default role must allow viewing the guild")
|
|
}
|
|
}
|