2026-09-19 21:29:07 +03:00
|
|
|
package auth_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/pquerna/otp/totp"
|
|
|
|
|
|
|
|
|
|
"glchat/internal/auth"
|
|
|
|
|
"glchat/internal/config"
|
|
|
|
|
"glchat/internal/database"
|
|
|
|
|
"glchat/internal/permissions"
|
|
|
|
|
"glchat/internal/store"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
testPepper = "unit-test-session-pepper"
|
|
|
|
|
testMasterKey = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func testConfig() config.Config {
|
|
|
|
|
return config.Config{
|
|
|
|
|
SessionPepper: testPepper,
|
|
|
|
|
MasterKey: testMasterKey,
|
|
|
|
|
TOTPEncryptionKey: "unit-test-totp-key",
|
|
|
|
|
SessionTTLHours: 24,
|
|
|
|
|
// Низкие параметры Argon2id: тесты не должны работать секундами.
|
|
|
|
|
Argon2MemoryKiB: 1024,
|
|
|
|
|
Argon2Iterations: 1,
|
|
|
|
|
Argon2Parallelism: 1,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newService(t *testing.T) (*auth.Service, *store.Store) {
|
|
|
|
|
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)
|
|
|
|
|
service, err := auth.New(ctx, testConfig(), st, logger)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("auth.New: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return service, st
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// bootstrapMainGuild создаёт главный сервер с ролями по умолчанию, как это
|
|
|
|
|
// делает установщик (AGENT.md 7.3).
|
|
|
|
|
func bootstrapMainGuild(t *testing.T, st *store.Store, ownerID uint64) *store.Guild {
|
|
|
|
|
t.Helper()
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
guild, err := st.CreateGuild(ctx, store.CreateGuildParams{Name: "Главный сервер", OwnerID: ownerID, IsMain: true})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("create main guild: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := st.CreateRole(ctx, store.CreateRoleParams{
|
|
|
|
|
GuildID: guild.ID, Name: "Администратор", Permissions: uint64(permissions.AllPermissions), Position: 100,
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatalf("create admin role: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := st.CreateRole(ctx, store.CreateRoleParams{
|
|
|
|
|
GuildID: guild.ID, Name: "Пользователь", Permissions: uint64(permissions.DefaultUserPermissions),
|
|
|
|
|
Position: 0, IsDefault: true, Mentionable: true,
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatalf("create default role: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := st.SetInstanceSetting(ctx, "main_guild_id", itoa(guild.ID)); err != nil {
|
|
|
|
|
t.Fatalf("set main guild: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return guild
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// registerErr выполняет регистрацию и возвращает только ошибку: в тестах,
|
|
|
|
|
// где важен лишь результат проверки, это читается лучше вызова с пропусками.
|
|
|
|
|
func registerErr(service *auth.Service, in auth.RegisterInput) error {
|
|
|
|
|
_, _, _, err := service.Register(context.Background(), in)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func itoa(value uint64) string {
|
|
|
|
|
if value == 0 {
|
|
|
|
|
return "0"
|
|
|
|
|
}
|
|
|
|
|
digits := []byte{}
|
|
|
|
|
for value > 0 {
|
|
|
|
|
digits = append([]byte{byte('0' + value%10)}, digits...)
|
|
|
|
|
value /= 10
|
|
|
|
|
}
|
|
|
|
|
return string(digits)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRegisterAndLogin(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
service, st := newService(t)
|
|
|
|
|
|
|
|
|
|
user, token, session, err := service.Register(ctx, auth.RegisterInput{
|
|
|
|
|
Username: "alex", DisplayName: "Александр", Email: "Alex@Example.COM",
|
|
|
|
|
Password: "correct-horse-battery", IP: "203.0.113.5", UserAgent: "test-agent",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Register: %v", err)
|
|
|
|
|
}
|
2026-09-20 01:11:04 +03:00
|
|
|
// Сервер существует (создан установщиком), но новый пользователь в него
|
|
|
|
|
// не попадает: вход только по приглашению (решение пользователя).
|
2026-09-19 21:29:07 +03:00
|
|
|
bootstrapMainGuild(t, st, user.ID)
|
|
|
|
|
second, _, _, err := service.Register(ctx, auth.RegisterInput{
|
|
|
|
|
Username: "newcomer", Email: "newcomer@example.com", Password: "correct-horse-battery",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Register newcomer: %v", err)
|
|
|
|
|
}
|
|
|
|
|
newcomerGuilds, err := st.ListGuildsForUser(ctx, second.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ListGuildsForUser(newcomer): %v", err)
|
|
|
|
|
}
|
2026-09-20 01:11:04 +03:00
|
|
|
if len(newcomerGuilds) != 0 {
|
|
|
|
|
t.Fatalf("newcomer must start without guilds, got %+v", newcomerGuilds)
|
2026-09-19 21:29:07 +03:00
|
|
|
}
|
|
|
|
|
if user.ID == 0 || token == "" || session.ID == 0 {
|
|
|
|
|
t.Fatal("registration must return user, token and session")
|
|
|
|
|
}
|
|
|
|
|
if user.IsInstanceAdmin {
|
|
|
|
|
t.Fatal("regular user must not be instance admin")
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(user.PasswordHash, "correct-horse-battery") {
|
|
|
|
|
t.Fatal("password must not be stored in clear text")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Email зашифрован в БД и расшифровывается только сервисом.
|
|
|
|
|
encrypted, err := st.EncryptedEmail(ctx, user.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("EncryptedEmail: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(strings.ToLower(encrypted), "alex@example.com") {
|
|
|
|
|
t.Fatal("email must be stored encrypted")
|
|
|
|
|
}
|
|
|
|
|
decrypted, err := service.Email(ctx, user.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Email: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if decrypted != "alex@example.com" {
|
|
|
|
|
t.Fatalf("Email = %q, want normalized address", decrypted)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Вход по email в любом регистре.
|
|
|
|
|
logged, newToken, _, err := service.Login(ctx, auth.LoginInput{
|
|
|
|
|
Email: "ALEX@example.com", Password: "correct-horse-battery", IP: "203.0.113.5",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Login: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if logged.ID != user.ID || newToken == token {
|
|
|
|
|
t.Fatal("login must return the same user with a fresh token")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, _, _, err := service.Login(ctx, auth.LoginInput{
|
|
|
|
|
Email: "alex@example.com", Password: "wrong-password", IP: "203.0.113.5",
|
|
|
|
|
}); !errors.Is(err, auth.ErrInvalidCredentials) {
|
|
|
|
|
t.Fatalf("Login with wrong password = %v, want ErrInvalidCredentials", err)
|
|
|
|
|
}
|
|
|
|
|
if _, _, _, err := service.Login(ctx, auth.LoginInput{
|
|
|
|
|
Email: "nobody@example.com", Password: "correct-horse-battery",
|
|
|
|
|
}); !errors.Is(err, auth.ErrInvalidCredentials) {
|
|
|
|
|
t.Fatalf("Login with unknown email = %v, want ErrInvalidCredentials", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRegisterRejectsWeakAndLeakedPasswords(t *testing.T) {
|
|
|
|
|
service, _ := newService(t)
|
|
|
|
|
|
|
|
|
|
for name, password := range map[string]string{
|
|
|
|
|
"short": "short",
|
|
|
|
|
"leaked": "password",
|
|
|
|
|
} {
|
|
|
|
|
err := registerErr(service, auth.RegisterInput{
|
|
|
|
|
Username: "user_" + name, Email: name + "@example.com", Password: password,
|
|
|
|
|
})
|
|
|
|
|
if !errors.Is(err, auth.ErrWeakPassword) {
|
|
|
|
|
t.Fatalf("Register with %s password = %v, want ErrWeakPassword", name, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for name, username := range map[string]string{"short": "a", "bad": "with space", "cyrillic": "имя"} {
|
|
|
|
|
err := registerErr(service, auth.RegisterInput{
|
|
|
|
|
Username: username, Email: "valid_" + name + "@example.com", Password: "correct-horse-battery",
|
|
|
|
|
})
|
|
|
|
|
if !errors.Is(err, auth.ErrInvalidUsername) {
|
|
|
|
|
t.Fatalf("Register with %s username = %v, want ErrInvalidUsername", name, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRegisterRejectsDuplicates(t *testing.T) {
|
|
|
|
|
service, _ := newService(t)
|
|
|
|
|
first := auth.RegisterInput{Username: "duplicate", Email: "first@example.com", Password: "correct-horse-battery"}
|
|
|
|
|
if err := registerErr(service, first); err != nil {
|
|
|
|
|
t.Fatalf("first Register: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := registerErr(service, auth.RegisterInput{
|
|
|
|
|
Username: "duplicate", Email: "second@example.com", Password: "correct-horse-battery",
|
|
|
|
|
})
|
|
|
|
|
if !errors.Is(err, auth.ErrUsernameTaken) {
|
|
|
|
|
t.Fatalf("duplicate username = %v, want ErrUsernameTaken", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = registerErr(service, auth.RegisterInput{
|
|
|
|
|
Username: "another", Email: "FIRST@example.com", Password: "correct-horse-battery",
|
|
|
|
|
})
|
|
|
|
|
if !errors.Is(err, auth.ErrEmailTaken) {
|
|
|
|
|
t.Fatalf("duplicate email (case-insensitive) = %v, want ErrEmailTaken", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSessionRotationAndLogoutAll(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
service, _ := newService(t)
|
|
|
|
|
user, token, session, err := service.Register(ctx, auth.RegisterInput{
|
|
|
|
|
Username: "session_user", Email: "session@example.com", Password: "correct-horse-battery",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Register: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rotated, err := service.RotateSession(ctx, session.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("RotateSession: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if rotated == token {
|
|
|
|
|
t.Fatal("rotation must produce a new token")
|
|
|
|
|
}
|
|
|
|
|
if _, _, err := service.ResolveSession(ctx, token); !errors.Is(err, auth.ErrSessionExpired) {
|
|
|
|
|
t.Fatalf("old token must stop working, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, resolved, err := service.ResolveSession(ctx, rotated); err != nil || resolved.UserID != user.ID {
|
|
|
|
|
t.Fatalf("rotated token must resolve: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := service.LogoutAll(ctx, user.ID); err != nil {
|
|
|
|
|
t.Fatalf("LogoutAll: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, _, err := service.ResolveSession(ctx, rotated); !errors.Is(err, auth.ErrSessionExpired) {
|
|
|
|
|
t.Fatal("logout-all must revoke every session")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTOTPFlowAndStepUp(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
service, _ := newService(t)
|
|
|
|
|
user, _, session, err := service.Register(ctx, auth.RegisterInput{
|
|
|
|
|
Username: "totp_user", Email: "totp@example.com", Password: "correct-horse-battery",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Register: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setup, err := service.SetupTOTP(ctx, user.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("SetupTOTP: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if setup.Secret == "" || !strings.HasPrefix(setup.URL, "otpauth://totp/") {
|
|
|
|
|
t.Fatalf("unexpected setup payload: %+v", setup)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := service.EnableTOTP(ctx, user.ID, "000000"); !errors.Is(err, auth.ErrTOTPInvalid) {
|
|
|
|
|
t.Fatalf("EnableTOTP with bad code = %v, want ErrTOTPInvalid", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
code := totpCode(t, setup.Secret)
|
|
|
|
|
recovery, err := service.EnableTOTP(ctx, user.ID, code)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("EnableTOTP: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(recovery) != 8 {
|
|
|
|
|
t.Fatalf("recovery codes = %d, want 8", len(recovery))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Вход без кода требует 2FA, с кодом — проходит.
|
|
|
|
|
if _, _, _, err := service.Login(ctx, auth.LoginInput{
|
|
|
|
|
Email: "totp@example.com", Password: "correct-horse-battery",
|
|
|
|
|
}); !errors.Is(err, auth.ErrTOTPRequired) {
|
|
|
|
|
t.Fatalf("Login without TOTP = %v, want ErrTOTPRequired", err)
|
|
|
|
|
}
|
|
|
|
|
if _, _, _, err := service.Login(ctx, auth.LoginInput{
|
|
|
|
|
Email: "totp@example.com", Password: "correct-horse-battery", TOTPCode: totpCode(t, setup.Secret),
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatalf("Login with TOTP: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Резервный код работает один раз.
|
|
|
|
|
if _, _, _, err := service.Login(ctx, auth.LoginInput{
|
|
|
|
|
Email: "totp@example.com", Password: "correct-horse-battery", TOTPCode: recovery[0],
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatalf("Login with recovery code: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, _, _, err := service.Login(ctx, auth.LoginInput{
|
|
|
|
|
Email: "totp@example.com", Password: "correct-horse-battery", TOTPCode: recovery[0],
|
|
|
|
|
}); !errors.Is(err, auth.ErrTOTPInvalid) {
|
|
|
|
|
t.Fatalf("reused recovery code = %v, want ErrTOTPInvalid", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step-up: свежая сессия проходит, затем окно можно проверить повторно.
|
|
|
|
|
if err := service.RequireStepUp(ctx, user, session, "correct-horse-battery", totpCode(t, setup.Secret)); err != nil {
|
|
|
|
|
t.Fatalf("RequireStepUp: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := service.RequireStepUp(ctx, user, session, "correct-horse-battery", ""); err != nil {
|
|
|
|
|
t.Fatalf("RequireStepUp inside the window: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestInstanceAdminRequiresTOTP(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
service, st := newService(t)
|
|
|
|
|
user, _, _, err := service.Register(ctx, auth.RegisterInput{
|
|
|
|
|
Username: "admin", Email: "admin@example.com", Password: "correct-horse-battery",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Register: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := st.SetInstanceAdmin(ctx, user.ID, true); err != nil {
|
|
|
|
|
t.Fatalf("SetInstanceAdmin: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Без настроенного TOTP инстанс-админ не может войти (AGENT.md 7.19).
|
|
|
|
|
if _, _, _, err := service.Login(ctx, auth.LoginInput{
|
|
|
|
|
Email: "admin@example.com", Password: "correct-horse-battery",
|
|
|
|
|
}); !errors.Is(err, auth.ErrInstanceAdminTOTP) {
|
|
|
|
|
t.Fatalf("Login as admin without TOTP = %v, want ErrInstanceAdminTOTP", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setup, err := service.SetupTOTP(ctx, user.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("SetupTOTP: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := service.EnableTOTP(ctx, user.ID, totpCode(t, setup.Secret)); err != nil {
|
|
|
|
|
t.Fatalf("EnableTOTP: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, _, _, err := service.Login(ctx, auth.LoginInput{
|
|
|
|
|
Email: "admin@example.com", Password: "correct-horse-battery", TOTPCode: totpCode(t, setup.Secret),
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatalf("Login as admin with TOTP: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Отключить 2FA инстанс-админу нельзя.
|
|
|
|
|
if err := service.DisableTOTP(ctx, user.ID); !errors.Is(err, auth.ErrInstanceAdminTOTP) {
|
|
|
|
|
t.Fatalf("DisableTOTP for admin = %v, want ErrInstanceAdminTOTP", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestChangePasswordRevokesOtherSessions(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
service, _ := newService(t)
|
|
|
|
|
user, _, current, err := service.Register(ctx, auth.RegisterInput{
|
|
|
|
|
Username: "password_user", Email: "password@example.com", Password: "correct-horse-battery",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Register: %v", err)
|
|
|
|
|
}
|
|
|
|
|
_, otherToken, _, err := service.Login(ctx, auth.LoginInput{
|
|
|
|
|
Email: "password@example.com", Password: "correct-horse-battery",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("second Login: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := service.ChangePassword(ctx, user.ID, current.ID, "correct-horse-battery", "brand-new-password-1"); err != nil {
|
|
|
|
|
t.Fatalf("ChangePassword: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, _, err := service.ResolveSession(ctx, otherToken); !errors.Is(err, auth.ErrSessionExpired) {
|
|
|
|
|
t.Fatal("other sessions must be revoked after a password change")
|
|
|
|
|
}
|
|
|
|
|
if _, _, _, err := service.Login(ctx, auth.LoginInput{
|
|
|
|
|
Email: "password@example.com", Password: "correct-horse-battery",
|
|
|
|
|
}); !errors.Is(err, auth.ErrInvalidCredentials) {
|
|
|
|
|
t.Fatalf("old password must stop working, got %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, _, _, err := service.Login(ctx, auth.LoginInput{
|
|
|
|
|
Email: "password@example.com", Password: "brand-new-password-1",
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatalf("Login with new password: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRegistrationCanBeDisabled(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
service, st := newService(t)
|
|
|
|
|
if err := st.SetInstanceSetting(ctx, "registration_enabled", "false"); err != nil {
|
|
|
|
|
t.Fatalf("SetInstanceSetting: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, _, _, err := service.Register(ctx, auth.RegisterInput{
|
|
|
|
|
Username: "blocked", Email: "blocked@example.com", Password: "correct-horse-battery",
|
|
|
|
|
}); !errors.Is(err, auth.ErrRegistrationOff) {
|
|
|
|
|
t.Fatalf("Register with disabled registration = %v, want ErrRegistrationOff", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPasswordsAndSecretsAreNotLogged(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
var buf strings.Builder
|
|
|
|
|
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.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
|
|
|
service, err := auth.New(ctx, testConfig(), st, logger)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("auth.New: %v", err)
|
|
|
|
|
}
|
|
|
|
|
user, token, _, err := service.Register(ctx, auth.RegisterInput{
|
|
|
|
|
Username: "secret_user", Email: "secret@example.com", Password: "correct-horse-battery",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Register: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, _, _, err := service.Login(ctx, auth.LoginInput{
|
|
|
|
|
Email: "secret@example.com", Password: "correct-horse-battery",
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatalf("Login: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logs := buf.String()
|
|
|
|
|
for _, secret := range []string{"correct-horse-battery", token, testPepper, testMasterKey} {
|
|
|
|
|
if secret != "" && strings.Contains(logs, secret) {
|
|
|
|
|
t.Fatalf("logs contain a secret value: %s", secret)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Хэш пароля тоже не должен попадать в лог.
|
|
|
|
|
hash := user.PasswordHash
|
|
|
|
|
if hash != "" && strings.Contains(logs, hash) {
|
|
|
|
|
t.Fatal("logs contain the password hash")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// totpCode считает текущий код для секрета.
|
|
|
|
|
func totpCode(t *testing.T, secret string) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
code, err := totp.GenerateCode(secret, time.Now().UTC())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GenerateCode: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return code
|
|
|
|
|
}
|