package auth_test import ( "context" "io" "log/slog" "path/filepath" "runtime" "testing" "time" "update_server/internal/auth" "update_server/internal/config" "update_server/internal/db" ) func TestEnsureBootstrapAdminCreatesSingleAdminUser(t *testing.T) { t.Parallel() ctx := context.Background() store := newTestStore(t) defer store.Close() cfg := config.Config{ AdminEmail: "admin@example.com", AdminPassword: "correct horse battery staple", SessionCookieName: "update_server_session", SessionTTL: 24 * time.Hour, } logger := slog.New(slog.NewTextHandler(io.Discard, nil)) service := auth.NewService(cfg, logger, store) if err := service.EnsureBootstrapAdmin(ctx); err != nil { t.Fatalf("bootstrap admin: %v", err) } if err := service.EnsureBootstrapAdmin(ctx); err != nil { t.Fatalf("bootstrap admin second pass: %v", err) } user, err := store.Users.GetByEmail(ctx, cfg.AdminEmail) if err != nil { t.Fatalf("load bootstrapped admin: %v", err) } if user.Role != db.UserRoleAdmin { t.Fatalf("expected admin role, got %q", user.Role) } if !user.IsActive { t.Fatal("expected bootstrapped admin to be active") } if user.PasswordHash == cfg.AdminPassword { t.Fatal("expected bootstrapped password to be hashed") } if err := auth.ComparePassword(user.PasswordHash, cfg.AdminPassword); err != nil { t.Fatalf("compare hashed password: %v", err) } var userCount int if err := store.DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM users`).Scan(&userCount); err != nil { t.Fatalf("count users: %v", err) } if userCount != 1 { t.Fatalf("expected one bootstrapped user, got %d", userCount) } } func newTestStore(t *testing.T) *db.Store { t.Helper() sqlitePath := filepath.Join(t.TempDir(), "update-server.sqlite") database, err := db.Open(context.Background(), sqlitePath) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.Migrate(context.Background(), database, testProjectPath(t, "migrations")); err != nil { database.Close() t.Fatalf("migrate sqlite: %v", err) } return db.NewStore(database) } func testProjectPath(t *testing.T, parts ...string) string { t.Helper() _, filename, _, ok := runtime.Caller(0) if !ok { t.Fatal("resolve caller path") } root := filepath.Join(filepath.Dir(filename), "..", "..") items := append([]string{root}, parts...) return filepath.Join(items...) }